Google Cloud PubSub: не отправлять и не получать все сообщения из облачных функций - PullRequest
0 голосов
/ 03 апреля 2019

Сводка : мой клиентский код запускает 861 фоновую функцию Google Cloud, публикуя сообщения в пабе / подразделе. Каждая облачная функция выполняет задачу, загружает результаты в Google Storage и публикует сообщения в другой теме Pub / Sub, где прослушивается код клиента. Клиентский код не получает все сообщения, хотя все облачные функции выполнены (проверено по количеству результатов в Google Storage).

Серверная часть : у меня есть фоновая функция Google Cloud, которая запускается каждый раз, когда сообщение публикуется в пабе / подпрограмме TRIGGER. Пользовательские атрибуты данных сообщения действуют в качестве параметра функции, в зависимости от того, какая функция выполняет определенную задачу. Затем он загружает результат в корзину в Google Storage и публикует сообщение (с идентификатором задачи и сведениями о времени выполнения) в разделе «РЕЗУЛЬТАТЫ» / «Sub» (отличается от того, который использовался для запуска этой функции).

Клиентская сторона : мне нужно выполнить 861 различную задачу, которая требует вызова функции Cloud с 861 слегка отличающимися входами. Эти задачи похожи, и облачной функции для их выполнения требуется от 20 секунд до 2 минут (в среднем около 1 минуты). Для этого я создал скрипт на python, который запускаю из Google Cloud Shell (или оболочки локального компьютера). Клиентский скрипт Python публикует 861 сообщение в тему публикации / подписки TRIGGER, которая одновременно запускает столько облачных функций, каждому из которых передается уникальный идентификатор задачи в ярости [0, 860]. Затем клиентский сценарий Python опрашивает тему RESULTS Pub / Sub «синхронным способом» для любых сообщений. Облачная функция после выполнения задачи публикует сообщение в раздел РЕЗУЛЬТАТЫ Pub / Sub с уникальным идентификатором задачи и информацией о времени. Этот уникальный идентификатор задачи используется клиентом для определения, из какой задачи отправлено сообщение. Это также помогает идентифицировать дубликаты сообщений, которые отбрасываются.

Основные шаги :

  1. Сценарий клиентского Python публикует 861 сообщение (каждое с уникальным идентификатором задачи) в теме публикации / подпункта TRIGGER и ожидает сообщений о результатах от облачной функции.
  2. Вызывается 861 различных облачных функций, каждая из которых выполняет задачу, загружает результаты в Google Storage и публикует сообщение (с идентификатором taskID и сроками выполнения) в публикации / подкатегории RESULTS.
  3. Клиент получает все сообщения синхронно и помечает задачу как выполненную.

Задача : Когда клиент опрашивает сообщения из раздела РЕЗУЛЬТАТОВ Pub / Sub, я не получаю сообщения для всех taskID. Я уверен, что облачная функция была вызвана и выполнена правильно (у меня 861 результатов в корзине Google Storage). Я повторял это несколько раз, и это происходило каждый раз. Странно, но число пропущенных TaskID меняется каждый раз, а также различные TaskID пропадают при разных запусках. Я также отслеживаю количество полученных дубликатов TaskID. Количество уникальных идентификаторов задач, полученных, отсутствующих и повторных, приведены в таблице для 5 независимых прогонов.

SN   # of Tasks  Received  Missing  Repeated
1     861          860      1        25
2     861          840      21       3
3     861          851      10       1
4     861          837      24       3
5     861          856      5        1

Я не уверен, откуда может возникнуть эта проблема. Учитывая случайный характер числа, а также пропущенных пропущенных идентификаторов задач, я подозреваю, что в логике публикации хотя бы раз в публикации Pub / Sub есть какая-то ошибка. Если в облачной функции я сплю несколько секунд вместо выполнения задачи, например, с помощью time.sleep (5), то все работает просто отлично (я получаю все 861 taskID на клиенте).

Код для воспроизведения этой проблемы.

В дальнейшем main.py вместе с requirements.txt развертываются как облачная функция Google, а client.py - это код клиента. Запустите клиент с 100 одновременными задачами как python client.py 100, что повторяет его 5 раз. Каждый раз пропадает различное количество taskID.

requirements.txt

google-cloud-pubsub

main.py

"""
This file is deployed as Google Cloud Function. This function starts,
sleeps for some seconds and pulishes back the taskID.

Deloyment:
    gcloud functions deploy gcf_run --runtime python37 --trigger-topic <TRIGGER_TOPIC> --memory=128MB --timeout=300s
"""

import time
from random import randint
from google.cloud import pubsub_v1

# Global variables
project_id = "<Your Google Cloud Project ID>"  # Your Google Cloud Project ID
topic_name = "<RESULTS_TOPIC>"  # Your Pub/Sub topic name


def gcf_run(data, context):
    """Background Cloud Function to be triggered by Pub/Sub.
    Args:
         data (dict): The dictionary with data specific to this type of event.
         context (google.cloud.functions.Context): The Cloud Functions event
         metadata.
    """

    # Message should contain taskID (in addition to the data)
    if 'attributes' in data:
        attributes = data['attributes']
        if 'taskID' in attributes:
            taskID = attributes['taskID']
        else:
            print('taskID missing!')
            return
    else:
        print('attributes missing!')
        return

    # Sleep for a random time beteen 30 seconds to 1.5 minutes
    print("Start execution for {}".format(taskID))
    sleep_time = randint(30, 90)  # sleep for this many seconds
    time.sleep(sleep_time)  # sleep for few seconds

    # Marks this task complete by publishing a message to Pub/Sub.
    data = u'Message number {}'.format(taskID)
    data = data.encode('utf-8')  # Data must be a bytestring
    publisher = pubsub_v1.PublisherClient()
    topic_path = publisher.topic_path(project_id, topic_name)
    publisher.publish(topic_path, data=data, taskID=taskID)

    return

client.py

"""
The client code creates the given number of tasks and publishes to Pub/Sub,
which in turn calls the Google Cloud Functions concurrently.
Run:
    python client.py 100
"""

from __future__ import print_function
import sys
import time
from google.cloud import pubsub_v1

# Global variables
project_id = "<Google Cloud Project ID>" # Google Cloud Project ID
topic_name = "<TRIGGER_TOPIC>"    # Pub/Sub topic name to publish
subscription_name = "<subscriber to RESULTS_TOPIC>"  # Pub/Sub subscription name
num_experiments = 5  # number of times to repeat the experiment
time_between_exp = 120.0 # number of seconds between experiments

# Initialize the Publisher (to send commands that invoke Cloud Functions)
# as well as Subscriber (to receive results written by the Cloud Functions)
# Configure the batch to publish as soon as there is one kilobyte
# of data or one second has passed.
batch_settings = pubsub_v1.types.BatchSettings(
    max_bytes=1024,  # One kilobyte
    max_latency=1,   # One second
)
publisher = pubsub_v1.PublisherClient(batch_settings)
topic_path = publisher.topic_path(project_id, topic_name)

subscriber = pubsub_v1.SubscriberClient()
subscription_path = subscriber.subscription_path(
    project_id, subscription_name)


class Task:
    """
    A task which will execute the Cloud Function once.

    Attributes:
        taskID (int)       : A unique number given to a task (starting from 0).
        complete (boolean) : Flag to indicate if this task has completed.
    """
    def __init__(self, taskID):
        self.taskID = taskID
        self.complete = False

    def start(self):
        """
        Start the execution of Cloud Function by publishing a message with
        taskID to the Pub/Sub topic.
        """
        data = u'Message number {}'.format(self.taskID)
        data = data.encode('utf-8')  # Data must be a bytestring
        publisher.publish(topic_path, data=data, taskID=str(self.taskID))

    def end(self):
        """
        Mark the end of this task.
            Returns (boolean):
                True if normal, False if task was already marked before.
        """
        # If this task was not complete, mark it as completed
        if not self.complete:
            self.complete = True
            return True

        return False
    # [END of Task Class]


def createTasks(num_tasks):
    """
    Create a list of tasks and return it.
        Args:
            num_tasks (int) : Number of tasks (Cloud Function calls)
        Returns (list):
            A list of tasks.
    """
    all_tasks = list()
    for taskID in range(0, num_tasks):
        all_tasks.append(Task(taskID=taskID))

    return all_tasks


def receiveResults(all_tasks):
    """
    Receives messages from the Pub/Sub subscription. I am using a blocking
    Synchronous Pull instead of the usual asynchronous pull with a callback
    funtion as I rely on a polling pattern to retrieve messages.
    See: https://cloud.google.com/pubsub/docs/pull
        Args:
            all_tasks (list) : List of all tasks.
    """
    num_tasks = len(all_tasks)
    total_msg_received = 0  # track the number of messages received
    NUM_MESSAGES = 10  # maximum number of messages to pull synchronously
    TIMEOUT = 600.0    # number of seconds to wait for response (10 minutes)

    # Keep track of elapsed time and exit if > TIMEOUT
    __MyFuncStartTime = time.time()
    __MyFuncElapsedTime = 0.0

    print('Listening for messages on {}'.format(subscription_path))
    while (total_msg_received < num_tasks) and (__MyFuncElapsedTime < TIMEOUT):
        # The subscriber pulls a specific number of messages.
        response = subscriber.pull(subscription_path,
            max_messages=NUM_MESSAGES, timeout=TIMEOUT, retry=None)
        ack_ids = []

        # Keep track of all received messages
        for received_message in response.received_messages:
            if received_message.message.attributes:
                attributes = received_message.message.attributes
                taskID = int(attributes['taskID'])
                if all_tasks[taskID].end():
                    # increment count only if task completes the first time
                    # if False, we received a duplicate message
                    total_msg_received += 1
                #     print("Received taskID = {} ({} of {})".format(
                #         taskID, total_msg_received, num_tasks))
                # else:
                #     print('REPEATED: taskID {} was already marked'.format(taskID))
            else:
                print('attributes missing!')

            ack_ids.append(received_message.ack_id)

        # Acknowledges the received messages so they will not be sent again.
        if ack_ids:
            subscriber.acknowledge(subscription_path, ack_ids)

        time.sleep(0.2)  # Wait 200 ms before polling again
        __MyFuncElapsedTime = time.time() - __MyFuncStartTime
        # print("{} s elapsed. Listening again.".format(__MyFuncElapsedTime))

    # if total_msg_received != num_tasks, function exit due to timeout
    if total_msg_received != num_tasks:
        print("WARNING: *** Receiver timed out! ***")
    print("Received {} messages out of {}. Done.".format(
        total_msg_received, num_tasks))


def main(num_tasks):
    """
    Main execution point of the program
    """

    for experiment_num in range(1, num_experiments + 1):
        print("Starting experiment {} of {} with {} tasks".format(
            experiment_num, num_experiments, num_tasks))
        # Create all tasks and start them
        all_tasks = createTasks(num_tasks)
        for task in all_tasks:     # Start all tasks
            task.start()
        print("Published {} taskIDs".format(num_tasks))

        receiveResults(all_tasks)  # Receive message from Pub/Sub subscription

        print("Waiting {} seconds\n\n".format(time_between_exp))
        time.sleep(time_between_exp)  # sleep between experiments


if __name__ == "__main__":
    if(len(sys.argv) != 2):
        print("usage: python client.py  <num_tasks>")
        print("    num_tasks: Number of concurrent Cloud Function calls")
        sys.exit()

    num_tasks = int(sys.argv[1])
    main(num_tasks)

1 Ответ

6 голосов
/ 04 апреля 2019

В вашей облачной функции, в этой строке:

publisher.publish (topic_path, data = data, taskID = taskID)

Вы не ждете будущего, которое вернет publisher.publish. Это означает, что вы не можете быть уверены, что публикация в теме действительно произошла, когда вы опускаетесь до конца функции gcf_run, но сообщение о подписке на облачные функции темы TRIGGER в любом случае ACK-ed.

Вместо этого, чтобы дождаться завершения публикации для завершения облачной функции, это должно быть:

publisher.publish(topic_path, data=data, taskID=taskID).result()

Вам также следует избегать вызова и разрушения клиента-издателя при каждом вызове функции, вместо этого иметь клиент в качестве глобальной переменной.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...