paint-brush
Cómo enviar millones de notificaciones push con Go y Firebase Cloud Messagingpor@vgukasov
16,664 lecturas
16,664 lecturas

Cómo enviar millones de notificaciones push con Go y Firebase Cloud Messaging

por Vladislav Gukasov2021/07/11
Read on Terminal Reader
Read this story w/o Javascript

Demasiado Largo; Para Leer

Las notificaciones automáticas son mensajes cortos que un recurso web envía a sus suscriptores en computadoras y dispositivos móviles. Notificaciones como esa devuelven al usuario al sitio (o aplicación) que ya ha visitado. Las capacidades técnicas de las notificaciones push permiten desarrollar una estrategia de marketing más productiva y “responsiva” para un producto. Usaremos Golang y Firebase Cloud Messaging para enviar mensajes push en nuestra aplicación. El servicio suele ser un microservicio, y Go se adapta perfectamente a eso porque funciona simultáneamente, utilizando la CPU tanto como sea posible.

People Mentioned

Mention Thumbnail

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Cómo enviar millones de notificaciones push con Go y Firebase Cloud Messaging
Vladislav Gukasov HackerNoon profile picture

¡Hola a todos! Mi nombre es Vladislav Gukasov. Soy ingeniero de software en una empresa fintech en el equipo de comunicaciones. Una de las herramientas de comunicación más efectivas que usamos hoy en día es el servicio de notificaciones automáticas que envía decenas de millones de mensajes por día.


Si desarrolla un sistema de notificaciones automáticas por primera vez, puede ser complicado. Te diré cómo configurar un servicio, enviar mensajes push y evitar algunos errores que pueden causar un bajo rendimiento.

Notificaciones push

En primer lugar, debemos tener claro qué son las notificaciones push. Las notificaciones automáticas son mensajes cortos que un recurso web envía a sus suscriptores en computadoras y dispositivos móviles. Notificaciones como esa devuelven al usuario al sitio (o aplicación) que ya ha visitado. Las capacidades técnicas de las notificaciones push permiten desarrollar una estrategia de marketing más productiva y “responsiva” para un producto.


Por qué debería usar notificaciones automáticas en su aplicación:

  • lo más probable es que un usuario vea tu mensaje
  • es gratis (a diferencia de los SMS y correos electrónicos)
  • privacidad: el usuario no le proporciona ningún dato personal. Te comunicas a través de un token de inserción impersonal

Arquitectura de servicio

Pasemos a la implementación del servicio de envío de notificaciones push. Usaremos Golang y Firebase Cloud Messaging. Un poco más de detalle por qué:

Vamos

  • El servicio de notificación suele ser un microservicio, y Go se adapta muy bien a eso.
  • funciona simultáneamente, utilizando la CPU tanto como sea posible, lo que brinda un alto rendimiento
  • tiene herramientas integradas de creación de perfiles / evaluación comparativa

Mensajería en la nube de Firebase

  • solución gratuita de google
  • solo puede enviar notificaciones a Android a través de Firebase
  • hay SDK para la mayoría de los lenguajes de programación


La integración con Firebase tiene lugar tanto en el lado del cliente como en el del servidor. Todas las plataformas de clientes deben instalar el debido SDK y generar un token de inserción. Los tokens terminados se envían al servidor: el servidor almacena los tokens en el almacenamiento.

Esquema de ahorro de token push

Cuando cualquier servicio necesita enviar una notificación, obtenemos tokens de inserción del almacenamiento y enviamos un mensaje.
Esquema de despacho de mensajes push

SDK Instalación e inicialización

Lo primero que debemos hacer es instalar el paquete Firebase

 go get firebase.google.com/go


Antes de que podamos usar el SDK, necesitamos obtener los créditos de autorización. Para hacer esto, debe crear un nuevo proyecto en Firebase console y obtener las credenciales de autenticación de la cuenta de servicio .


Ahora podemos inicializar el cliente en nuestro código.

 import ( "context" firebase "firebase.google.com/go" "firebase.google.com/go/messaging" "google.golang.org/api/option" ) // There are different ways to add credentials on init. // if we have a path to the JSON credentials file, we use the GOOGLE_APPLICATION_CREDENTIALS env var os.Setenv("GOOGLE_APPLICATION_CREDENTIALS", c.Firebase.Credentials) // or pass the file path directly opts := []option.ClientOption{option.WithCredentialsFile("creds.json")} // if we have a raw JSON credentials value, we use the FIREBASE_CONFIG env var os.Setenv("FIREBASE_CONFIG", "{...}") // or we can pass the raw JSON value directly as an option opts := []option.ClientOption{option.WithCredentialsJSON([]byte("{...}"))} app, err := firebase.NewApp(ctx, nil, opts...) if err != nil { log.Fatalf("new firebase app: %s", err) } fcmClient, err := app.Messaging(context.TODO()) if err != nil { log.Fatalf("messaging: %s", err) }

Cómo usar Firebase con proxy

En algunos proyectos, el servicio de correo está ubicado en la red interna y no tiene acceso al mundo exterior. Para acceder a las API externas, debe usar un proxy. El cliente de FCM realiza 2 tipos de solicitudes HTTP: recibe tokens de acceso OAuth y envía notificaciones. Así es como podemos inicializar un cliente con un proxy.

 import ( "context" firebase "firebase.google.com/go" "firebase.google.com/go/messaging" "google.golang.org/api/option" ) proxyURL := "http://localhost:10100" // insert you proxy here // The SDK makes 2 different types of calls: // 1. To the Google OAuth2 service to fetch the refresh and access tokens. // 2. To Firebase to send the pushes. // Each type uses its own HTTP Client and we need to insert our custom HTTP Client with proxy everywhere. cl := &http.Client{ Transport: &http.Transport{Proxy: http.ProxyURL(proxyURL)}, } ctxWithClient := context.WithValue(ctx, oauth2.HTTPClient, cl) // This is how we insert our custom HTTP Client in the Google OAuth2 service: // by context with specific value. creds, err := google.CredentialsFromJSON(ctxWithClient, []byte(c.Firebase.Credentials), firebaseScopes...) if err != nil { log.Fatalf("google credentials from JSON: %s", err) } // And this is how we insert proxy for the Firebase calls. Initialize base transport with our proxy. tr := &oauth2.Transport{ Source: creds.TokenSource, Base: &http.Transport{Proxy: http.ProxyURL(proxyURL)}, } hCl := &http.Client{ Transport: tr, Timeout: 10 * time.Second, } opts := []option.ClientOption{option.WithHTTPClient(hCl)} app, err := firebase.NewApp(ctx, nil, opts...) if err != nil { log.Fatalf("new firebase app: %s", err) } fcmClient, err := app.Messaging(context.TODO()) if err != nil { log.Fatalf("messaging: %s", err) }

Enviemos algunas notificaciones push

El despacho push común es muy simple. Pasamos el título, el cuerpo y el token de inserción del cliente, y se envía la notificación.

 response, err := fcmClient.Send(ctx, &messaging.Message{ Notification: &messaging.Notification{ Title: "A nice notification title", Body: "A nice notification body", }, Token: "client-push-token", // a token that you received from a client })


Echemos un vistazo al rendimiento de la implementación utilizando la herramienta de evaluación comparativa integrada de Go.

El benchmark es sintético. Hacemos llamadas API reales, pero no enviamos una notificación ya que Firebase tiene limitación. El código de referencia está aquí .

 Sent 590 push messages by service Benchmark_Service_SendPush-8 590 2099685 ns/op


Vemos que cada llamada tarda una media de 2,1 ms, y enviamos solo 590 notificaciones en 1,24 s. Por lo tanto, la implementación puede ser adecuada para usted si tiene pocos usuarios y acaba de comenzar a enviar notificaciones.

Mejora del rendimiento

Sin embargo, la implementación descrita anteriormente no es suficiente para enviar millones de notificaciones. No queremos hacer N llamadas porque cada mensaje impone una sobrecarga en milisegundos. Necesitamos agrupar mensajes en lotes y enviar cientos de mensajes a la vez. Para ello, implementaremos un búfer simple. Firebase proporciona una API por lotes para estos casos. En respuesta, recibirá una matriz, cuyos índices son los mismos que la matriz de mensajes enviados por usted.

 import ( "context" "log" "sync" "time" "firebase.google.com/go/messaging" ) // Buffer batches all incoming push messages and send them periodically. type Buffer struct { fcmClient *messaging.Client dispatchInterval time.Duration batchCh chan *messaging.Message wg sync.WaitGroup } func (b *Buffer) SendPush(msg *messaging.Message) { b.batchCh <- msg } func (b *Buffer) sender() { defer b.wg.Done() // set your interval t := time.NewTicker(b.dispatchInterval) // we can send up to 500 messages per call to Firebase messages := make([]*messaging.Message, 0, 500) defer func() { t.Stop() // send all buffered messages before quit b.sendMessages(messages) log.Println("batch sender finished") }() for { select { case m, ok := <-b.batchCh: if !ok { return } messages = append(messages, m) case <-tC: b.sendMessages(messages) messages = messages[:0] } } } func (b *Buffer) Run() { b.wg.Add(1) go b.sender() } func (b *Buffer) Stop() { close(b.batchCh) b.wg.Wait() } func (b *Buffer) sendMessages(messages []*messaging.Message) { if len(messages) == 0 { return } batchResp, err := b.fcmClient.SendAll(context.TODO(), messages) log.Printf("batch response: %+v, err: %s \n", batchResp, err) }


Echemos un vistazo al rendimiento de la implementación del búfer.

 Sent 1513794 push messages by buffer Benchmark_Buffer_SendPush-8 1513794 677.9 ns/op


Vemos que cada llamada tarda en promedio 0,00068 ms y enviamos ~ 1,5 millones de notificaciones en 1,02 s. Eso es un gran impulso que puede manejar una gran cantidad de mensajes de inserción rápidamente. Para mejoras adicionales, podemos simplemente escalar horizontalmente nuestro servicio.

Conclusión

El rendimiento del envío de notificaciones push depende de muchos factores que son exclusivos de cada proyecto. Hemos considerado ejemplos de mejoras desde el lado del código, pero hay muchos puntos de crecimiento más allá de eso.


De todos modos, vale la pena centrarse ante todo en los usuarios y sus necesidades. Si una implementación simple de "llamadas API N" funciona correctamente en su proyecto, entonces no debe optimizar y complicar prematuramente el código. Cuanto más simple sea su sistema, más barato será mantenerlo y más rápido verán los resultados sus usuarios.