推送通知是让用户立即接收到事件的一个非常有效的工具。在Gojek,我们每天需要处理300多万个订单,跨20多款产品。
可以想象的是,我们每天推送的通知数量有多大——大概每小时1百万个。这篇文章将介绍我们在处理如此体量的推送通知时所面临的挑战,以及我们的解决方案。
体量大还只是其中的一个方面,在Gojek,我们还需要面对一些独有的问题。
Gojek不只有一个App,除了用户App,我们还有GoLife、司机App、商家App,还有服务商App。
当我们的系统要推送通知,要么是推给某个用户的App(例如,推给GoLife的通知不会被推到Gojek上),要么是推给所有的App(例如促销通知)。
我们的系统需要足够灵活,能够在广播和单独推送之间自由地选择。
因为我们的用户App需要支持iOS和Android两个平台,所以也需要支持多个通知系统。
Android平台我们使用了FCM(Firebase Cloud Messaging)和GCM(Google Cloud Messaging),iOS平台我们使用了APNS(Apple Push Notification Service)。
每一个通知服务提供商都为不同的App提供了不同的API秘钥和令牌。例如,GoLife和Gojek使用的FCM API秘钥就不一样。
我们允许一个用户同时登录多个设备,所以通知需要被推送给用户已登录的所有设备上,这就存在之前的两个问题:
Gojek采用了微服务架构,我们想要让每个服务都能推送通知,不需要操心多设备和多服务提供商问题。
为了解决上述问题,并尽可能保持API简单,我们的通知系统被分为三个组件:
每个组件都解决了上述的一部分问题,接下来,我们来深入介绍这些组件。
用户在登录App后,App会使用设备令牌和App ID调用令牌存储API。
在用户退出时,这些记录会被删除。
令牌存储用于决定向用户的哪些设备推送通知。
这是HTTP服务器,提供用于推送通知的API。
为了简单起见,API要求把用户ID和App ID放在HTTP头部,把通知信息放在请求体里:
POST http://<base_url>/notification
user_id: <user_id>
application_id: <application_id>
{
"payload": {},
"title": "You driver is here",
"message": "Please meet your driver at the pickup point"
}
服务器从令牌存储获取所有的用户设备信息,然后为每个用户设备安排一个调度作业。
通知服务器为系统提供了外部接口,需要推送通知的服务只要通过用户ID来调用它的API,剩下的事情由通知服务负责处理。
我们使用RabbitMQ作为作业队列,并为每一种App ID和通知类型创建了单独的队列。
分配单独的队列是很重要的,因为我们要为每一种App和通知类型做好故障隔离。例如,如果com.gojek.app的FCM令牌过期,就不会影响到com.gojek.life或者com.gojek.driver.bike的作业。
处理器进程从作业队列里拉取消息,把它们发送给对应的通知服务提供商。
为了保持代码简单,并能够支持不同的服务提供商,我们定义了统一接口:
type PushService interface {
Push(ctx context.Context, m PushRequest) (PushResponse, error)
}
Push方法接收一个请求对象,并返回一个响应对象。
请求对象包含了与接收方和通知(比如过期时间、标题和文本)有关的信息。
type PushRequest struct {
DeviceID string
Title string
Message string
Payload map[string]interface{}
//其他参数
}
响应消息里包含了通知是否发送成功的信息:
type PushResponse struct {
Success bool
ErrorMsg string
}
然后为不同的服务提供商实现接口。例如,FCM和APNS对应的实现看起来像下面这样:
type FCMProvider struct {
// 配置信息,比如API令牌和URL端点
}
func (p *FCMProvider) Push(ctx context.Context, m queue.Message) (notification.PushResponse, error) {
// 发送通知给FCM服务器
}
type APNSProvider struct {
// 配置信息,比如API令牌和URL端点
}
func (p *APNSProvider) Push(ctx context.Context, m queue.Message) (notification.PushResponse, error) {
// 发送通知给APNS服务器
}
通知处理器负责选择对应的通知服务提供商,并将消息发送给它们。
在面对这些挑战时,我们找出其中的一些常用模式,把它们抽离成不同的服务,将一个相对复杂的问题变成了一系列简单且易于管理的服务。
每当一个核心逻辑需要不同的实现时,我们就把它抽离成单独的服务:
最终,我们构建了一个每小时能够处理1百多万个推送通知的系统。
领取专属 10元无门槛券
私享最新 技术干货