• בלוג
  • איך מגבילים מספר פעולות בדקה

איך מגבילים מספר פעולות בדקה

21/04/2020

מערכות תוכנה טובות יודעות לנצל בצורה מיטבית את משאבי המחשב עליו הן רצות. אחד המנגנונים שעוזרים לנו לעשות את זה הוא Rate Limit, וזה עובד ככה: בגלל שהמערכת שלנו מריצה מספר תהליכים, אם ניתן לתהליך אחד לרוץ "הכי מהר שרק אפשר" התהליך הזה עלול לתפוס משאבים הנחוצים לתהליכים אחרים במערכת.

הרבה פעמים מנגנון Rate Limit משמש בקוד תקשורת. כשאנחנו כותבים תוכנה שמושכת מידע מ API אנחנו לא רוצים למשוך את המידע "מהר" מדי, כי אז ה API עלול לנעול אותנו בחוץ. כשאנחנו כותבים את ה API אנחנו לא רוצים לאפשר ללקוח בודד למשוך יותר מדי מידע מהר מדי, כי אז לא יישארו לנו משאבים לטפל בלקוחות האחרים.

דרך אחת למימוש Rate Limiter היא שימוש ב Semaphore. כתבתי פעם דוגמא ב Python בקישור כאן. הסמפור מאותחל עם מספר טוקנים וכל בקשה מקבילית לוקחת אסימון, וכך כשנגמרים האסימונים הבקשה הבאה תיתקע עד שאחת אחרת תסיים. זה עובד אבל לא ממש עוזר לנו אם כל בקשות הרשת שלנו חוזרות מאוד מהר, ומה שאנחנו רוצים זה להגביל את מספר הבקשות בדקה.

טכניקה אחרת פשוטה למימוש מגבלת בקשות בדקה אפשר למצוא בקוד של Limiter, שהיא ג'ם של רובי למימוש מגבלה זו. הרעיון שלהם עובד כך:

  1. מאתחיל מערך כך שהגודל שלו יהיה זהה למגבלת הבקשות בדקה שאתם צריכים. לדוגמא אם אנחנו רוצים מגבלה של 10 בקשות בדקה ניצור מערך של 10 תאים.

  2. בכל תא במערך רושמים את השעה הישנה ביותר שמחשב תומך בה (מיוצגת על ידי המספר 0 - מספר השניות מאז ה 1/1/1970).

  3. כל פעם שלקוח מבקש לבצע פעולה אנחנו לוקחים את התא הבא במערך, מוסיפים לו את האינטרוול (כלומר דקה) ובודקים אם התוצאה היא בעתיד. אם כן מחכים עד שהתוצאה תגיע. אם לא מאפשרים את ביצוע הפעולה במיידי.

  4. מעדכנים את תוכן התא בזמן הנוכחי, ועוברים לתא הבא בשביל הלקוח הבא (המעבר לתא הבא משתמש במודולו כדי לרוץ על המערך בצורה מעגלית).

כל עוד מספר הבקשות בדקה קטן מהאינטרוול, כשנחבר לתא הבא את האינטרוול נקבל זמן בעבר והכל ירוץ מהר. אבל ברגע שמתחיל עומס המערכת תיעצר עד שתעבור דקה מהבקשה הישנה ביותר ואז תמשיך לרוץ, וכך בכל אינטרוול של דקה לעולם לא יהיו יותר מ n בקשות במקביל, לפי גודל המערך.

הפונקציה המרכזית ברובי מהקוד היא:

    def shift
      time = nil

      @mutex.synchronize do
        time = @ring[@head]

        sleep_until(time + @interval)

        @ring[@head] = Time.now
        @head = (@head + 1) % @size
      end

      time
    end

ואת שאר המחלקה תוכלו למצוא בקוד של הג'ם בקישור https://github.com/Shopify/limiter/blob/master/lib/limiter/rate_queue.rb.