מדריך ליצירת שירות צ'אט ב-WebSocket ל-Cloud Run

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

אפשר להגדיר את Cloud Run כך שישתמש בזיקה לסשן (session affinity), אבל הזיקה הזו היא במאמץ מרבי, כלומר כל בקשה חדשה עדיין יכולה להיות מנותבת למכונה אחרת. כתוצאה מכך, צריך לסנכרן את הודעות המשתמשים בשירות הצ'אט בכל המכונות, ולא רק בין הלקוחות שמחוברים למכונה אחת.

תכנון שירות צ'אט בזמן אמת

שירות הצ'אט לדוגמה משתמש במכונה של Memorystore for Redis כדי לאחסן ולסנכרן את הודעות המשתמשים בכל המכונות. כדי לדחוף נתונים ללקוחות מנויים שמחוברים לכל מכונה, וכדי לבטל את הסקרים של HTTP לעדכונים, Redis משתמש במנגנון Pub/Sub (לא להתבלבל עם המוצר Cloud Pub/Sub).

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

מכונות Redis מוגנות מפני האינטרנט באמצעות כתובות IP פרטיות, עם גישה מבוקרת ומוגבלת לשירותים שפועלים באותה רשת וירטואלית פרטית כמו מכונת Redis. מומלץ להשתמש בתעבורת נתונים יוצאת ישירה של VPC.

מגבלות

  • במדריך הזה לא מוצגים אימות של משתמשי קצה או שמירת סשנים במטמון. למידע נוסף על אימות משתמשי קצה, עיינו במדריך של Cloud Run בנושא אימות משתמשי קצה.

  • במדריך הזה לא מטמיעים מסד נתונים כמו Firestore לאחסון ולאחזור של היסטוריית הודעות הצ'אט ללא הגבלת זמן.

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

מטרות

  • לכתוב, לפתח ולפרוס שירות Cloud Run שמשתמש ב-WebSockets.

  • מתחברים למכונה של Memorystore for Redis כדי לפרסם הודעות חדשות ולהירשם אליהן במכונות שונות.

  • מחברים את שירות Cloud Run ל-Memorystore באמצעות תעבורת נתונים יוצאת (egress) ישירה של VPC.

עלויות

במסמך הזה משתמשים ברכיבים הבאים של Google Cloud, והשימוש בהם כרוך בתשלום:

בעזרת מחשבון העלויות אתם יכולים להעריך את העלויות בהתאם לשימוש הצפוי.

משתמשים חדשים של Google Cloud ? יכול להיות שאתם זכאים לתקופת ניסיון בחינם.

לפני שמתחילים

  1. נכנסים לחשבון Google Cloud . אנחנו ממליצים למשתמשים חדשים ב- Google Cloud ליצור חשבון כדי שיוכלו להעריך את הביצועים של המוצרים שלנו בתרחישים מהעולם האמיתי. לקוחות חדשים מקבלים בחינם גם קרדיט בשווי 300$ להרצה, לבדיקה ולפריסה של עומסי העבודה.
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator role (roles/resourcemanager.projectCreator), which contains the resourcemanager.projects.create permission. Learn how to grant roles.

    Go to project selector

  3. Verify that billing is enabled for your Google Cloud project.

  4. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator role (roles/resourcemanager.projectCreator), which contains the resourcemanager.projects.create permission. Learn how to grant roles.

    Go to project selector

  5. Verify that billing is enabled for your Google Cloud project.

  6. מפעילים את ממשקי ה-API של Cloud Run,‏ Memorystore for Redis,‏ Artifact Registry ו-Cloud Build, אם הם עדיין לא מופעלים.

    התפקידים שנדרשים כדי להפעיל ממשקי API

    כדי להפעיל ממשקי API, צריך את ההרשאה serviceusage.services.enable. אם יצרתם את הפרויקט, סביר להניח שכבר יש לכם את ההרשאה הזו דרך התפקיד "בעלים" (roles/owner). אחרת, תוכלו לקבל את ההרשאה הזו דרך התפקיד "אדמין בממשק 'שימוש בשירות'" (roles/serviceusage.serviceUsageAdmin). איך מקצים תפקידים

    הפעלת ממשקי ה-API

  7. מתקינים ומפעילים את ה-CLI של gcloud.

התפקידים הנדרשים

כדי לקבל את ההרשאות שדרושות להשלמת המדריך, בקשו מהאדמין לתת לכם את התפקידים הבאים ב-IAM ברמת הפרויקט:

להסבר על מתן תפקידים, ראו איך מנהלים את הגישה ברמת הפרויקט, התיקייה והארגון.

יכול להיות שאפשר לקבל את ההרשאות הנדרשות גם באמצעות תפקידים בהתאמה אישית או תפקידים מוגדרים מראש.

הגדרת ברירות המחדל של gcloud

כדי להגדיר את gcloud עם ברירות מחדל לשירות Cloud Run:

  1. מגדירים את פרויקט ברירת המחדל:

    gcloud config set project PROJECT_ID

    מחליפים את PROJECT_ID בשם הפרויקט שיצרתם בשביל המדריך הזה.

  2. מגדירים את gcloud לאזור שבחרתם:

    gcloud config set run/region REGION

    מחליפים את REGION באזור הנתמך של Cloud Run לבחירתכם.

מיקומים של Cloud Run

‫Cloud Run הוא שירות אזורי, כלומר התשתית שמפעילה את שירותי Cloud Run נמצאת באזור ספציפי ומנוהלת על ידי Google, כך שהיא זמינה באופן יתיר בכל האזורים באותו אזור.

הגורמים העיקריים לבחירת האזור שבו יפעלו שירותי Cloud Run הם עמידה בדרישות של זמן אחזור, זמינות או עמידות. בדרך כלל אפשר לבחור את האזור הכי קרוב למשתמשים, אבל כדאי להביא בחשבון את המיקום של המוצרים Google Cloudהאחרים שמשמשים את שירות Cloud Run. שימוש במוצרי Google Cloud ביחד בכמה מיקומים יכול להשפיע על זמן האחזור של השירות וגם על העלות.

אפשר להשתמש ב-Cloud Run באזורים הבאים:

בכפוף לתמחור ברמה 1

בכפוף לתמחור ברמה 2

  • africa-south1 (יוהנסבורג)
  • asia-east2 (הונג קונג)
  • asia-northeast3 (סיאול, דרום קוריאה)
  • asia-southeast1 (סינגפור)
  • asia-southeast2 (ג'קרטה)
  • asia-south2 (דלהי, הודו)
  • australia-southeast1 (סידני)
  • australia-southeast2 (מלבורן)
  • europe-central2 (ורשה, פולין) סמל של עלה רמה נמוכה של CO2
  • europe-west10 (ברלין)
  • ‫europe-west12 (טורינו) סמל של עלה רמה נמוכה של CO2
  • ‫europe-west2 (לונדון, בריטניה) סמל של עלה רמה נמוכה של CO2
  • europe-west3 (פרנקפורט, גרמניה)
  • europe-west6 (ציריך, שווייץ) סמל של עלה רמה נמוכה של CO2
  • me-central1 (דוחא)
  • me-central2 (דמאם)
  • ‫northamerica-northeast1 (מונטריאול) סמל של עלה רמה נמוכה של CO2
  • ‫northamerica-northeast2 (טורונטו) סמל של עלה רמה נמוכה של CO2
  • ‫southamerica-east1 (סאו פאולו, ברזיל) סמל של עלה רמה נמוכה של CO2
  • southamerica-west1 (סנטיאגו, צ'ילה) סמל של עלה רמה נמוכה של CO2
  • us-west2 (לוס אנג'לס)
  • us-west3 (סולט לייק סיטי)
  • us-west4 (לאס וגאס)

אם כבר יצרתם שירות Cloud Run, אתם יכולים לראות את האזור במרכז הבקרה של Cloud Run במסוףGoogle Cloud .

אחזור של דוגמת הקוד

כדי לאחזר את דוגמת הקוד לשימוש:

  1. משכפלים את המאגר לדוגמה ומעבירים אותו למכונה המקומית:

    Node.js

    git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git

    אפשר גם להוריד את הדוגמה כקובץ ZIP ולחלץ אותו.

  2. עוברים לספרייה שמכילה את הקוד לדוגמה של Cloud Run:

    Node.js

    cd nodejs-docs-samples/run/websockets/

הסבר על הקוד של WebSockets

‫Socket.io היא ספרייה שמאפשרת תקשורת דו-כיוונית בזמן אמת בין הדפדפן לשרת. למרות ש-Socket.io הוא לא הטמעה של WebSocket, הוא כולל את הפונקציונליות כדי לספק API פשוט יותר לכמה פרוטוקולי תקשורת עם תכונות נוספות, כמו אמינות משופרת, חיבור מחדש אוטומטי ושידור לכל הלקוחות או לקבוצת משנה שלהם.

שילוב בצד הלקוח

<script src="/socket.io/socket.io.js"></script>

הלקוח יוצר מופע חדש של Socket לכל חיבור. הדוגמה הזו עוברת עיבוד בצד השרת, ולכן לא צריך להגדיר את כתובת ה-URL של השרת. מופע ה-socket יכול לשלוח אירועים ולהאזין להם.

// Initialize Socket.io
const socket = io('', {
  transports: ['websocket'],
});
// Emit "sendMessage" event with message
socket.emit('sendMessage', msg, error => {
  if (error) {
    console.error(error);
  } else {
    // Clear message
    $('#msg').val('');
  }
});
// Listen for new messages
socket.on('message', msg => {
  log(msg.user, msg.text);
});

// Listen for notifications
socket.on('notification', msg => {
  log(msg.title, msg.description);
});

// Listen connect event
socket.on('connect', () => {
  console.log('connected');
});

שילוב בצד השרת

בצד השרת, שרת Socket.io מאותחל ומצורף לשרת ה-HTTP. בדומה לצד הלקוח, ברגע ששרת Socket.io יוצר חיבור ללקוח, נוצר מופע של שקע לכל חיבור שאפשר להשתמש בו כדי לשלוח הודעות ולהאזין להן. בנוסף, Socket.io מספק ממשק ליצירת "חדרים" או ערוץ שרירותי ששקעים יכולים להצטרף אליו ולעזוב אותו.

// Initialize Socket.io
const server = require('http').Server(app);
const io = require('socket.io')(server);

const {createAdapter} = require('@socket.io/redis-adapter');
// Replace in-memory adapter with Redis
const subClient = redisClient.duplicate();
io.adapter(createAdapter(redisClient, subClient));
// Add error handlers
redisClient.on('error', err => {
  console.error(err.message);
});

subClient.on('error', err => {
  console.error(err.message);
});

// Listen for new connection
io.on('connection', socket => {
  // Add listener for "signin" event
  socket.on('signin', async ({user, room}, callback) => {
    try {
      // Record socket ID to user's name and chat room
      addUser(socket.id, user, room);
      // Call join to subscribe the socket to a given channel
      socket.join(room);
      // Emit notification event
      socket.in(room).emit('notification', {
        title: "Someone's here",
        description: `${user} just entered the room`,
      });
      // Retrieve room's message history or return null
      const messages = await getRoomFromCache(room);
      // Use the callback to respond with the room's message history
      // Callbacks are more commonly used for event listeners than promises
      callback(null, messages);
    } catch (err) {
      callback(err, null);
    }
  });

  // Add listener for "updateSocketId" event
  socket.on('updateSocketId', async ({user, room}) => {
    try {
      addUser(socket.id, user, room);
      socket.join(room);
    } catch (err) {
      console.error(err);
    }
  });

  // Add listener for "sendMessage" event
  socket.on('sendMessage', (message, callback) => {
    // Retrieve user's name and chat room  from socket ID
    const {user, room} = getUser(socket.id);
    if (room) {
      const msg = {user, text: message};
      // Push message to clients in chat room
      io.in(room).emit('message', msg);
      addMessageToCache(room, msg);
      callback();
    } else {
      callback('User session not found.');
    }
  });

  // Add listener for disconnection
  socket.on('disconnect', () => {
    // Remove socket ID from list
    const {user, room} = deleteUser(socket.id);
    if (user) {
      io.in(room).emit('notification', {
        title: 'Someone just left',
        description: `${user} just left the room`,
      });
    }
  });
});

‫Socket.io מספק גם מתאם Redis כדי לשדר אירועים לכל הלקוחות, בלי קשר לשרת שמשרת את ה-socket. ‫Socket.io משתמש רק במנגנון Pub/Sub של Redis ולא מאחסן נתונים.

const {createAdapter} = require('@socket.io/redis-adapter');
// Replace in-memory adapter with Redis
const subClient = redisClient.duplicate();
io.adapter(createAdapter(redisClient, subClient));

המתאם של Redis ב-Socket.io יכול להשתמש מחדש בלקוח Redis שמשמש לאחסון היסטוריית ההודעות של החדר. כל קונטיינר ייצור חיבור למכונה של Redis, ו-Cloud Run יכול ליצור מספר גדול של מכונות. זה הרבה פחות מ-65,000 החיבורים ש-Redis יכול לתמוך בהם.

חיבור מחדש

זמן הקצוב המקסימלי לתפוגה ב-Cloud Run הוא 60 דקות. לכן צריך להוסיף לוגיקה של חיבור מחדש למקרים של זמן קצוב לתפוגה. במקרים מסוימים, Socket.io מנסה להתחבר מחדש באופן אוטומטי אחרי אירועים של ניתוק או שגיאת חיבור. אין ערובה לכך שהלקוח יתחבר מחדש לאותו מכונה.

// Listen for reconnect event
socket.io.on('reconnect', () => {
  console.log('reconnected');
  // Emit "updateSocketId" event to update the recorded socket ID with user and room
  socket.emit('updateSocketId', {user, room}, error => {
    if (error) {
      console.error(error);
    }
  });
});
// Add listener for "updateSocketId" event
socket.on('updateSocketId', async ({user, room}) => {
  try {
    addUser(socket.id, user, room);
    socket.join(room);
  } catch (err) {
    console.error(err);
  }
});

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

שליחת המכשיר לתיקון

  1. יוצרים מכונה של Memorystore for Redis:

    gcloud redis instances create INSTANCE_ID --size=1 --region=REGION

    מחליפים את מה שכתוב בשדות הבאים:

    • ‫INSTANCE_ID: שם המכונה, לדוגמה my-redis-instance.
    • ‫REGION_ID: האזור של כל המשאבים והשירותים, לדוגמה europe-west1.

    למכונה יוקצה באופן אוטומטי טווח כתובות IP מתוך טווח ברירת המחדל של רשת השירות. במדריך הזה משתמשים בזיכרון בנפח 1GB למטמון המקומי של ההודעות במכונה של Redis. איך קובעים את הגודל הראשוני של מכונה ב-Memorystore בהתאם לתרחיש לדוגמה.

  2. מגדירים משתנה סביבה עם כתובת ה-IP של הרשת המורשית של מכונת Redis:

     export REDISHOST=$(gcloud redis instances describe INSTANCE_ID --region REGION --format "value(host)")
  3. יוצרים חשבון שירות שישמש כזהות בשירות. כברירת מחדל, אין לו הרשאות מלבד חברות בפרויקט.

    gcloud iam service-accounts create chat-identity
    gcloud projects add-iam-policy-binding PROJECT_ID \
    --member=serviceAccount:chat-identity@PROJECT_ID.iam.gserviceaccount.com \
    --role=roles/serviceusage.serviceUsageConsumer
  4. מריצים את הפקודה הבאה כדי למצוא את השם של רשת ה-VPC המורשית של מכונת Redis:

      gcloud redis instances describe INSTANCE_ID --region REGION --format "value(authorizedNetwork)"
    

    מחליפים את מה שכתוב בשדות הבאים:

    • ‫INSTANCE_ID: שם המכונה, לדוגמה my-redis-instance.
    • ‫REGION_ID: האזור של כל המשאבים והשירותים, לדוגמה europe-west1.

    רושמים את השם של רשת ה-VPC.

  5. יוצרים את קובץ האימג' של הקונטיינר ופורסים אותו ב-Cloud Run:

    gcloud run deploy chat-app --source . \
        --allow-unauthenticated \
        --timeout 3600 \
        --service-account chat-identity \
        --network NETWORK \
        --subnet SUBNET \
        --update-env-vars REDISHOST=$REDISHOST

    מחליפים את מה שכתוב בשדות הבאים:

    • ‫NETWORK הוא השם של רשת ה-VPC המורשית שאליה מחובר המכונה של Redis.
    • ‫SUBNET הוא השם של רשת המשנה. רשת המשנה חייבת להיות בגודל /26 או יותר. תעבורת נתונים יוצאת (egress) ב-Direct VPC תומכת בטווחי IPv4 מסוג RFC 1918,‏ RFC 6598 ו-Class E.

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

לנסות את השירות

כדי לנסות את השירות המלא:

  1. עוברים בדפדפן לכתובת ה-URL שסופקה בשלב הפריסה.

  2. כדי להיכנס, צריך להוסיף את השם שלכם וחדר צ'אט.

  3. שליחת הודעה לחדר

אם תבחרו להמשיך לפתח את השירותים האלה, זכרו שיש להם גישה מוגבלת לניהול זהויות והרשאות גישה (IAM) לשאר השירותים של Google Cloud , וצריך לתת להם תפקידי IAM נוספים כדי לגשת לשירותים רבים אחרים.

הסרת המשאבים

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

מחיקת הפרויקט

אם יצרתם פרויקט חדש בשביל המדריך הזה, מחקו את הפרויקט. אם השתמשתם בפרויקט קיים ואתם רוצים לשמור אותו בלי השינויים שהוספתם במדריך הזה, מחקו את המשאבים שיצרתם בשביל המדריך.

הדרך הקלה ביותר לבטל את החיוב היא למחוק את הפרויקט שיצרתם בשביל המדריך.

כדי למחוק את הפרויקט:

  1. במסוף Google Cloud , נכנסים לדף Manage resources.

    כניסה לדף Manage resources

  2. ברשימת הפרויקטים, בוחרים את הפרויקט שרוצים למחוק ולוחצים על Delete.
  3. כדי למחוק את הפרויקט, כותבים את מזהה הפרויקט בתיבת הדו-שיח ולוחצים על Shut down.

מחיקת משאבי המדריך

  1. מוחקים את שירות Cloud Run שפרסתם במדריך הזה. שירותי Cloud Run לא צוברים עלויות עד שהם מקבלים בקשות.

    כדי למחוק את השירות ב-Cloud Run, מריצים את הפקודה הבאה:

    gcloud run services delete SERVICE-NAME

    מחליפים את SERVICE-NAME בשם השירות.

    אפשר גם למחוק שירותי Cloud Run ממסוףGoogle Cloud .

  2. מסירים את הגדרת ברירת המחדל של האזור gcloud שהוספתם במהלך הגדרת המדריך:

     gcloud config unset run/region
    
  3. מסירים את הגדרות הפרויקט:

     gcloud config unset project
    
  4. מוחקים משאבים אחרים של Google Cloud שנוצרו במדריך הזה:

המאמרים הבאים