¡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.
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:
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é:
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.
Cuando cualquier servicio necesita enviar una notificación, obtenemos tokens de inserción del almacenamiento y enviamos un mensaje.
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) }
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) }
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.
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.
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.