বেশির ভাগ ওয়েব অ্যাপ ব্যাকগ্রাউন্ড কিউ থেকে উপকৃত হতে পারে, যা প্রায়শই ত্রুটিসংভাব্য বা সময়সাপেক্ষ সাইড জব প্রসেস করতে ব্যবহৃত হয়। এই ব্যাকগ্রাউন্ড জবগুলো ইমেইল পাঠানোর থেকে ক্যাশ আপডেট করার পর্যায়, বা কোর বিজনেস লজিক সম্পাদন করা পর্যন্ত ভিন্ন হতে পারে।
যে কোনো ব্যাকগ্রাউন্ড কিউইং সিস্টেম যেহেতু প্রসেস করার জন্য জবের সংখ্যা স্কেল করে, তো জবগুলো প্রসেস করা ওয়ার্কারদের পুলও স্কেল করতে হয়। যে কেসগুলোতে জব এনকিউ করার রেট পরিবর্তনশীল, সেখানে কিউ ওয়ার্কারদের সংখ্যা স্কেল আপ করা প্রসেসিং স্পিড বজায় রাখতে একটি কী আспек্ট। এছাড়া, কম কিউ থ্রুপুটের সময় ওয়ার্কার স্কেল ডাউন করলে উল্লেখযোগ্য বাঁচত থাকতে পারে!
দুর্ভাগ্যক্রমে, অনেক কিউইং ব্যাকএন্ড স্কেলিং লজিক নিয়ে আসে না যা ওয়ার্কার অন বা অফ করবে। কিন্তু আমরা কিছু সিম্পল গণিত এবং পারফরম্যান্স ডেটা ব্যবহার করে আমাদের অপ্টিমাল ওয়ার্কার কাউন্ট বের করতে পারি কিউতে অপেক্ষমান কাজের উপর ভিত্তি করে।
কিউইং রুল অফ থাম্ব
যদি জবগুলো এনকিউ হয় daha 높은 রেটে থেকে সেগুলো কিউ ওয়ার্কার প্রসেস করে, তবে কিউর গভীরতা বাড়বে এবং প্রতিটি জব কিউতে কাটানো সময়ও বাড়বে। সাধারণত, আমরা প্রতিটি জবের জন্য ওয়েট টাইম (কিউতে কাটানো সময়) যতটা সম্ভব কম রাখতে চাই — ০ সেকেন্ড থেকে কিছু গ্রহণযোগ্য লিমিট পর্যন্ত। কিউতে অপেক্ষমান জবের উপর ভিত্তি করে একটি ডিজায়ারড ওয়েট টাইম সেটিসফাই করার জন্য প্রয়োজনীয় ওয়ার্কার সংখ্যা অনুমান করতে, আমরা কিউইং রুল অফ থাম্ব (QROT) ব্যবহার করতে পারি। সাধারণত, QROT একটি অসমতা হিসেবে এক্সপ্রেস করা হয় যা একটি জব কিউ সার্ভিস করার জন্য প্রয়োজনীয় সার্ভার সংখ্যা বর্ণনা করে, কিন্তু একটি ফর্ম এভাবে লেখা যায়:
workers = (number_of_jobs * avg_service_time_per_job) / time_to_finish_queue
তো, যদি আমরা চাই কিউ সার্ভিস করার জন্য প্রয়োজনীয় ওয়ার্কার সংখ্যা বের করা একটি ডিজায়ারড টাইমে, মনে করুন ৩০ সেকেন্ড, তবে আমাদের শুধু জানতে হবে জবের সংখ্যা (কিউর সাইজ) এবং প্রতিটি জব এক্সিকিউট করার গড় সময়। উদাহরণস্বরূপ, যদি আমাদের ৭৫০০ জবের একটি কিউ থাকে এবং প্রতিটি জব গড়ে ০.৩ সেকেন্ড নেয় এক্সিকিউট করতে, তবে আমরা সেটি ৩০ সেকেন্ডে ফিনিশ করতে পারি ৭৫ ওয়ার্কারের সাথে।
পারফরম্যান্স মেট্রিক্স অ্যাক্সেস করা
কিউতে জবের গড় সার্ভিস টাইম অনুমান করতে, আমাদের প্রতিটি জব ক্লাসের জন্য পারফরম্যান্স মেট্রিক্স অ্যাক্সেস দরকার। सौভাগ্যক্রমে, অ্যাপসিগনেল কমন কিউইং ব্যাকএন্ডের জন্য আউট-অফ-দ-বক্স পারফরম্যান্স ডেটা রেকর্ড করে, প্রতিবার একটি জব এক্সিকিউট হলে মেট্রিক্স রেকর্ড করে।
আমরা আপকামিং অ্যাপসিগনেল GraphQL API ব্যবহার করতে পারি গত ২৪ ঘন্টার প্রতিটি জব টাইপের গড় ডিউরেশন পেতে। এই API এখনো পূর্ণভাবে পাবলিক নয়, যদিও এটি বর্তমানে অ্যাপসিগনেলের পারফরম্যান্স গ্রাফ এবং অন্যান্য ডেটা ডিসপ্লে জন্য ব্যবহার হচ্ছে। সৌভাগ্যক্রমে, GraphQL API-গুলো সেলফ-ডকুমেন্টেড হলে ইচ্ছুক, এবং আমরা GraphiQL-এর মতো একটি টুল ব্যবহার করতে পারি API ইন্ট্রোস্পেক্ট করতে এবং খুঁজে বের করতে এটি কী ডেটা অবজেক্ট এক্সপোজ করে।
GraphQL কোয়ারি বানানো প্রক্রিয়া এই পোস্টের স্কোপের বাইরে, কিন্তু নিচে একটি উদাহরণ রুবি ক্লাস দেওয়া আছে যা ফারডে HTTP ক্লায়েন্ট লাইব্রেরি ব্যবহার করে অ্যাপসিগনেল GraphQL API-এর সাথে কানেক্ট করে একটি বেসিক মেট্রিক্স এগরিগেশন কোয়ারি করার জন্য।
require 'json' require 'faraday' class AppsignalClient BASE_URL = 'https://appsignal.com/' DEFAULT_APP_ID = ENV['APPSIGNAL_APP_ID'] DEFAULT_TOKEN = ENV['APPSIGNAL_API_TOKEN'] # GraphQL কোয়ারি সিলেক্টেড অ্যাপের জন্য "mean" মেট্রিক ফেচ করতে। METRICS_QUERY = <<~GRAPHQL.freeze query($appId: String!, $query: [MetricAggregation!]!, $timeframe: TimeframeEnum!) { app(id: $appId) { metrics { list(timeframe: $timeframe, query: $query) { start end rows { fields { key value } } } } } } GRAPHQL def initialize(app_id: DEFAULT_APP_ID, client_secret: DEFAULT_TOKEN) @app_id = app_id @client_secret = client_secret end # একটি জব ক্লাসের পারফর্ম অ্যাকশনের গড় ডিউরেশন ফেচ করুন # ডিফল্ট টাইমফ্রেম হল গত ২৪ ঘন্টা def average_job_duration(job_class, timeframe: 'R24H') response = connection.post( 'graphql', JSON.dump( query: METRICS_QUERY, variables: { appId: @app_id, timeframe: timeframe, query: [ name: 'transaction_duration', headerType: legacy tags: [ { key: 'namespace', value: 'background' }, { key: 'action', value: "#{job_class.name}#perform" }, ], fields: [{ field: 'MEAN', aggregate: 'AVG' }], ], } ) ) data = JSON.parse(response.body, symbolize_names: true) rows = data.dig(:data, :app, :metrics, :list, :rows) # সিলেক্টেড টাইমফ্রেমে কোনো মেট্রিক নাও থাকতে পারে return 0.0 if rows.empty? rows.first[:fields].first[:value] end private def connection @connection ||= Faraday.new( url: BASE_URL, params: { token: @client_secret }, headers: { 'Content-Type' => 'application/json' }, request: { timeout: 10 } ) do |faraday| faraday.response :raise_error faraday.adapter Faraday.default_adapter end end end
এই ক্লাস দিয়ে, আমরা একটি প্রদত্ত ActiveJob ক্লাসের গড় জব ডিউরেশন পেতে পারি, যেটি মিলিসেকেন্ডে রিটার্ন হয়:
AppsignalClient.new.average_job_duration(MyMailerJob) # => 233.1
ডিফল্টভাবে, এটি গত ২৪ ঘন্টার জবের গড় ট্রানজ্যাকশন ডিউরেশন জন্য কল করে। যদি আমাদের জব(গুলো) এর চেয়ে অনেক বেশি ফ্রিকোয়েন্টলি এক্সিকিউট হয়, তবে আমরা সেই উইন্ডো ছোট করতে চাইতে পারি, সাম্প্রতিক এক্সিকিউশনকে আমাদের গড়ে বেশি ওজন দিয়ে। উদাহরণস্বরূপ, যদি আমাদের জব থাকে যেগুলো ঘণ্টায় সাঁইরো বার এক্সিকিউট হয়, তবে আমরা আমাদের timeframe এক ঘণ্টা (R1H) এ পরিবর্তন করতে পারি যাতে একে যেকোনো জবের ডিউরেশন ভালোভাবে অনুমান করা যায় যদি সেটি এখন এক্সিকিউট হয়।
লক্ষ্য করুন যে এই পারফরম্যান্স ডেটা আমাদের সার্ভার ইউটিলাইজেশন ডেটা থেকে আলাদা। এই ডেটা আমাদের বলে কতক্ষণ লাগবে আসলে প্রতিটি জবের জন্য প্রয়োজনীয় কাজ করতে। এটি আমাদের ওয়ার্কার স্কেল করার জন্য এক্সটার্নাল মেজারমেন্ট seperti ইউটিলাইজেশন মেট্রিকের চেয়ে বেশি উপকারী হবে।
কিউ ইন্ট্রোস্পেক্ট করা
পরবর্তী, আমাদের কিউ ইন্ট্রোস্পেক্ট করতে হবে সার্ভিস করা জবগুলো নির্ধারণ করতে। একটি সাধারণ রুবি কিউইং ব্যাকএন্ড হল Resque, যা ActiveJob-এর সাথে সুন্দরভাবে ইন্টিগ্রেট হয়। আমরা একটি দিয়ে কিউতে এনকিউড জবগুলো অ্যাক্সেস করতে পারি Resque-এ এবং তারপর প্রতিটি জবের এক্সিকিউশন টাইম অনুমান করতে পারি তার ক্লাসের উপর ভিত্তি করে, আমাদের উপরের AppsignalClient ক্লাস ব্যবহার করে।
require 'resque' class ResqueEstimator def initialize(queue: 'default') @queue = queue @cache = {} @appsignal_client = AppsignalClient.new end def enqueued_duration_estimate Resque.data_store.everything_in_queue(queue).map do |job| estimate_job_duration decode_activejob_args(job) end.sum end def estimate_job_duration(job) @cache[job['job_class']] ||= @appsignal_client .average_job_duration job['job_class'] end private # ActiveJob-স্পেসিফিক মেথড জব আর্গুমেন্ট পার্স করার জন্য # ActiveJob+Resque ইন্টিগ্রেশনের জন্য def decode_activejob_args(job) decoded_job = job decoded_job = Resque.decode(job) if job.is_a? String decoded_job['args'].first end end
এই ক্লাস ব্যবহার করা খুব সহজ:
ResqueEstimator.new(queue: 'my_queue').enqueued_duration_estimate # => 23000 (ms)
লক্ষ্য করুন যে আমরা জব ডিউরেশনের একটি সিম্পল মেমোয়াইজেশন ব্যবহার করছি আমাদের estimate_job_duration মেথডে যাতে অ্যাপসিগনেল API-এ ডুপ্লিকেট কল এড়ানো যায়। সবচেয়ে সম্ভবত, আমাদের কিউতে একই ক্লাসের অনেক জব থাকবে এবং আমরা আমাদের ওভারহেড কমাতে পারি প্রতিটি ক্লাসের এক্সিকিউশন একবার অনুমান করে।
পারফরম্যান্স ডেটা ব্যবহার করে স্কেল করা
সব কিছু একসাথে করলে, আমরা এখন আমাদের সাম্প্রতিক পারফরম্যান্স ডেটা ব্যবহার করে আমাদের কিউ ওয়ার্কার স্কেল আপ বা ডাউন করতে পারি আমাদের কিউর কন্টেন্টের উপর ভিত্তি করে! যেকোনো মুহূর্তে, আমরা আমাদের কিউতে জবগুলো দেখতে পারি এবং আমাদের ডিজায়ারড টাইম লিমিটে সেটি সার্ভিস করার জন্য প্রয়োজনীয় ওয়ার্কার অনুমান করতে পারি।
আমাদের একটি ডিজায়ারড কিউইং টাইম লিমিট ডিসাইড করতে হবে (যেকোনো জব কিউতে অপেক্ষা করার সর্বোচ্চ সময়), উদাহরণস্বরূপ ৩০ সেকেন্ড। আমাদের একটি মিনিমাম এবং ম্যাক্সিমাম ওয়ার্কার কাউন্টও স্পেসিফাই করতে হবে। কমপক্ষে এক ওয়ার্কার চালু রাখা হেল্পফুল কিউর জন্য, যাতে প্রথম জব(গুলো) হ্যান্ডল করা যায় কিউ খালি থাকার পর কিছুক্ষণ। আমরা একটি ম্যাক্সিমাম ওয়ার্কার কাউন্টও চাইবেন, ওভার-স্কেলিং এড়াতে আমাদের ডেটাবেস কানেকশন এবং/অথবা সার্ভার ইউটিলাইজেশন খরচে túl অনেক ওয়ার্কারের সাথে।
আমরা একটি ক্লাস তৈরি করতে পারি এই লজিক হ্যান্ডেল করার জন্য, যা মূলত আমাদের Queueing Rule of Thumb-এর ইমপ্লিমেন্টেশন।
class ResqueWorkerScaler def initialize(queue: 'default', workers_range: 1..100, desired_wait_ms: 300_000) @queue = queue @workers_range = workers_range @desired_wait_ms = desired_wait_ms @estimator = ResqueEstimator.new(queue: @queue) end def desired_workers total_time_ms = @estimator.enqueued_duration_estimate workers_required = [(total_time_ms / desired_wait_ms).ceil, workers_range.last].min [workers_required, workers_range.first].max end def scale # প্ল্যাটফর্ম-স্পেসিফিক স্কেলিং ইন্টারফেস ব্যবহার করে, desired_workers-এ স্কেল করুন end end
আমরা চাইব আমাদের ওয়ার্কারগুলো রেগুলার ইন্টারভালে স্কেল করা যাতে আমরা ডিমান্ডের উপর ভিত্তি করে স্কেল আপ এবং ডাউন করি। আমরা একটি রেক টাস্ক তৈরি করতে পারি যা আমাদের ResqueWorkerScaler ক্লাস কল করে ওয়ার্কার স্কেল করতে:
# inside lib/tasks/resque_workers.rake namespace :resque_workers do desc 'Scale worker pool based on enqueued jobs' task :scale, [:queue] => [:environment] do |_t, args| queue = args[:queue] || 'default' ResqueWorkerScaler.new(queue: queue).scale end end
তারপর আমরা একটি ক্রন জব সেট আপ করতে পারি এই রেক টাস্ককে রেগুলার ইন্টারভালে চালানোর জন্য:
*/5 * * * * /path/to/our/rake resque_workers:scale # scale a non-default queue: */5 * * * * /path/to/our/rake resque_workers:scale['my_queue']
লক্ষ্য করুন যে আমরা স্কেলিং টাস্ককে প্রতি ৫ মিনিটে চালানোর জন্য সেট করেছি। প্রতিটি নতুন ওয়ার্কার কিছুটা সময় নেয় অনলাইন আসতে এবং জব প্রসেস শুরু করতে — সম্ভবত ১০-৪০ সেকেন্ড যেকোনো জায়গা, আমাদের কোডবেসের আকার এবং আমরা কত জেম ব্যবহার করি তার ওপর নির্ভর করে। তো, যদি আমরা আমাদের ওয়ার্কার প্রতি মিনিটে স্কেল করার চেষ্টা করি, তবে আমরা সম্ভবত আবার স্কেল আপ বা ডাউন করব আমাদের ডিজায়ারড পরিবর্তনগুলো ইফেক্ট নেওয়ার আগে। যদি আমাদের অ্যাপ শুধু দিনের ভিন্ন সময়ে কিউ ইউজেস ফ্লাকচুয়েট করে দেখে, তবে আমরা সম্ভবত আমাদের রেক টাস্ককে আওয়ারলি ইন্টারভালে কল করতে পারি। কিন্তু যদি আমাদের কিউ সাইজ ঘন্টার ভিতরে ফ্লাকচুয়েট করে, তবে আমরা আমাদের কিউ ইন্ট্রোস্পেক্ট করতে চাবেন আরও ফ্রিকোয়েন্ট ইন্টারভালে, যেমন উপরের ৫ মিনিট।
পরবর্তী ধাপ
একটি সিস্টেম যেখানে আসল পারফরম্যান্স ডেটা ব্যবহার হয় ইনফ্রাস্ট্রাকচার স্কেল করার জন্য, সেটি ডিমান্ডের প্রতি খুব রেসপন্সিভ হতে পারে এবং ভিন্ন ব্যবহার প্যাটার্নের מול রেজিলিয়েন্ট। বিশেষ করে ব্যাকগ্রাউন্ড প্রসেসিং এর মতো এনভায়রনমেন্টে, যেখানে হোস্ট মেট্রিক যেমন মেমরি ইউজ এবং লোড গড় অনিয়মিত रूपে পরিবর্তন হয় না, পারফরম্যান্স মেট্রিক্স ব্যবহার করে স্কেল করা আরও উপযুক্ত।
বিকল্প কিউ স্কেলিং ইমপ্লিমেন্টেশন মানে পার জব গড় ওয়েট টাইম মাপতে পারে পুরো কিউ ইন্ট্রোস্পেক্ট করার বদলে, কিন্তু সেই মেট্রিক আনরিপ্রেজেন্টেটিভ হতে পারে যখন কিউ কন্টেন্ট এবং সাইজ দ্রুত পরিবর্তন হয়। যদি আমাদের সিস্টেম ব্যাপকভাবে ভেরিয়েবল লোড অনুভব করে, অনেক জব একসাথে এনকিউ হয়, বা ব্যাপকভাবে ভেরিয়েবল জব এক্সিকিউশন টাইম, তবে কিউ ইন্ট্রোস্পেকশন অনেক দ্রুত রেসপন্ড করে এবং বিশ্বস্তভাবে সঠিক।
কিন্তু আমাদের কিউ ইন্ট্রোস্পেকশন সিস্টেমে কিছু লিমিটেশন বিবেচনা করতে হবে। যদি কিউ পর্যাপ্ত বড় হয়, প্রতিটি জব দেখে এক্সিকিউশন অনুমান করা প্রহিবিটিভলি স্লো হবে। এসব কেসে, বেটার হল টোটাল জব কাউন্ট বের করা, তারপর কিউ থেকে একটি র্যান্ডম রিপ্রেজেন্টেটিভ স্যাম্পলিং জব সিলেক্ট করা এবং সেটি থেকে গড় এক্সিকিউশন ক্যালকুলেট করা। বিকল্পভাবে, যদি একটি জব ক্লাসের সাথে কোনো পারফরম্যান্স ডেটা অ্যাসোসিয়েটেড না থাকে এখনো, তবে আমাদের একটি অনুমান এক্সিকিউশন টাইম ব্যবহার করতে হবে যতক্ষণ না সেটি এক্সিকিউট হয় এবং কয়েকবার রেকর্ড হয়।
উপরে আউটলাইন করা সিস্টেমটি কিছু টুইকস দিয়ে উল্লেখযোগ্যভাবে উন্নত করা যায়। প্রতিটি জব ক্লাসের এক্সিকিউশন টাইম অনুমান করা প্যারালেলে বিবেচনা করুন, যেহেতু প্রতিটি অনুমান আইসোলেটেড এবং ইডেমপটেন্ট। আমরা আমাদের কিউ ইন্ট্রোস্পেকশন আপডেট করতে পারি সেই জবগুলো ইনক্লুড করে যেগুলো বর্তমানে একটি ওয়ার্কার দ্বারা এক্সিকিউট হচ্ছে আমাদের টোটাল সার্ভিস টাইম অনুমানের সঠিকতা উন্নত করার জন্য। একাধিক কিউ থাকার ব্যাকগ্রাউন্ড প্রসেসিং আর্কিটেকচারের জন্য, আমরা প্রতিটি কিউকে একটি ডিজায়ারড ওয়েট টাইম অ্যাসাইন করতে পারি কিউ প্রায়োরিটির ভিত্তিতে, এবং ওয়ার্কার যথাযথভাবে স্কেল করতে পারি।
কিউইং সিস্টেম প্রজেক্টের যেকোনো হাইলি ভেরিয়েবল কাজের অধিকাংশ সংগ্রহ করে। কিউ থেকে জব এক্সিকিউশনের পারফরম্যান্স ডেটা থাকলে, আমরা কার্যকরভাবে রিসোর্স স্কেল করতে পারি সকল কাজ সার্ভিস করার জন্য একটি রেসপন্সিভ, ইফিশিয়েন্ট পদ্ধতিতে।
হ্যাপি স্কেলিং!
P.S. আপনি যদি রুবি ম্যজিক পোস্টগুলো প্রেস থেকে বের होते সাথে সাথে পড়তে চান, আমাদের রুবি ম্যজিক নিউজলেটার সাবস্ক্রাইব করুন এবং কখনো একটি পোস্টও মিস করবেন না!