system design 俄罗斯大叔系列
消息系统

场景:需要发送消息以响应某些事件

like:信用卡交易金额超过限额时,需要通知持卡人

服务监控系统遇到大量API产生的故障,需要通知on-call工程师

抽象起来就是:一个publisher 生产消息,并传递给订阅者Subscribers

当发布者以某种顺序调用每个订阅者并等待响应时,我们可以在发布者和订阅者之间建立同步通信

难点:当订阅者和消息的数量增长时,系统难以扩展,并且难以扩展以支持不同类型的订阅者。

so,我们可以引入一个新系统,该系统可以注册任意数量的发布者和订阅者,并协调它们之间的消息传递

Untitled

Untitled

Requirement:

Functional:

define system behavior, or more specifically APIs - a set of operations the system will support.

  • createTopic(topicname)
  • publish(topicName, message)
  • subscribe(topicName, endpoint)

它相当于一个存储来自发布者的消息的存储桶,所有订阅者都从该存储桶接收消息的副本。

Non-Functional:
system qualities

  • scalable(support a big number of topics, publishers and subscribers)
  • highly available(可以容错,无单点错误)
  • highly performant (延迟低)

也可能要求其它性能like:安全、运营成本

high-level architecture

Untitled

client→LB→前端 → temporary storage→ sender

                 ↓→frontend → metadata service → metadata database

db & service: 前端连一个db,用来存储有关主题和订阅的信息,但是前端和db之间用一个service连接。

原因:
① 遵循设计原则:separation of concerns 关注点分离,可以通过定义好的接口访问DB。简化了维护和更改

② 该service将充当DB和其它components之间的缓存层,不想每条消息都hit DB,可以先从cache里面搜索。

temp storage: 前端后连一个临时storage,如果消息成功发送就会存很短时间,如果失败了,就会存时间稍微长点,以便重试

sender:还需要一个组件,从storage里检索消息并发送给订阅者

sender还需要call metadata service来检索订阅者的信息

当创建topic和调用API时,只需要将所有这些信息存储在数据库中。

遵循一些常见模式,like

负载均衡后面有一个前端

DB前面有一个service

it’s a 分布式缓存微服务 distributed cache microservice

FrontEnd Service

Untitled

a lightweight web service 轻量级web服务

负责:

  • request validation请求验证
  • authentication and authorization身份验证和授权
  • SSL termination SSL 终止
  • server-side encryption 服务器端加密
  • caching 缓存, throttling 限制
  • request dispatching and deduplucation 请求调度和去重
  • usage data collection 使用数据收集.

request到达host→进入reverse proxy反向代理

reverse proxy:轻量级服务器,负责:

① 当来自 HTTPS 的请求被解密并以未加密的形式进一步传递时,负责SSL终止;同时负责在将响应发送回客户端时对其进行加密

②compression压缩(like gzip):当响应返回给客户端之前,代理对其进行压缩。减少带宽用量。

③ 处理过慢的前端服务:如果前端服务变慢或者不可用,返回503

反向代理then把request给前端web service

cache:已知:对于每条发布的消息,前端服务都需要调用元数据服务来获取有关消息主题的信息。

所以,为了尽量减少对元数据服务的调用次数,前端可以使用本地缓存。like guava,或者自己创建LRU。

local disk:前端service还写了一堆log,记录

  • service logs agent:服务运行状况service health,服务异常exceptions

  • metrics agent:衡量指标

    key-value data,以后可能会被聚合并用于监控服务运行状况和收集统计信息,like 请求数量、故障faults、调用延迟call latency

  • audit logs agent:审计

    like 记录谁以及何时向系统中的特定 API 发出请求

** frontend service负责写入log,但实际的日志数据处理由其他组件管理,通常称为代理agents。

agents负责数据聚合aggregation并将日志传输到其他系统,进行后期处理和存储。

👆 职责分离有助于使前端服务更简单、更快、更健壮

下个组件:matadata service:

Untitled

一个Web 服务,负责在数据库中存储有关主题topic和订阅的信息

它是个distributed cache(如果消息过多,单个内存放不下). 在前端和storage之间

它读多写少

集群代表一个一致哈希环,每个前端host用某个键like id计算一个hash,like MD5,并根据这个值,前端host选择相应的metadata service

frontend host 如何知道要调用哪个metadata service?两种方法:

① 引入一个负责协调的component like zookeeper

它知道所有metadata service hosts,因为这些host不断给它发heartbeat

每个frontend service向它循环哪个metadata service host有我这个hash值的数据

每次scale更多metadata service时,这个configuration service都会知道,并重新映射remap hash

② 不用协调组件,确保每个frontend host都知道所有metadata host的信息。如果增删serice时,每个frontend host都会收到通知

有一些机制帮助frontend host发现metadata host,但略过,但提下Gossip 协议

它基于流行病传播的方式,随机对等选择一个host共享数据


一致性协议:

  1. 单主:2PC, Paxos, Raft
  2. 多主:Gossip (Redis)

接下来request来到temp storage,为什么叫temp,因为message停留时间很短。

Temporary Storage

Untitled

  • 必须快,高可用,可扩展,
  • 也需要持久性,如果订阅者不可用,message需要仍然存在

可能有多种设计option,like

DB:

① 涛SQL和NoSQL的pro con

→ 这里不需要ACID,不需要复杂的动态查询,不打算用这个存储来分析或当成数据仓库

但是需要方便读and写scale,高可用,并可以分区。

in all,用nosql

→ 消息大小有限(<1MB), so 不需要文档存储MongoDB

消息间无特定关系, so不用图形类

left:列类型 or key-value类型 like Cassandra Amazon DynamoDB

② 内存存储

要选能支持永久存储的 like redis

③ 消息队列

like kakfa sqs

④ further打动面试官: 流处理平台 stream-processing platforms

like kafka Amazon kinesis

然后message发送给订阅者

Sender

Untitled

(which 别种分布式系统也很好用, when 涉及数据检索、处理,and 消息fan out 并行发至多个目的地)

message retriever:

send做的第一件事是消息检索。通过一个线程池来实现的,其中每个线程都尝试从临时存储中读取数据。

可以实现一个简单方法,始终启动事先定好数量的消息检索线程。

但是有个问题是,有些线程可能是闲的,因为消息没那么多;另一方面,当消息过多时,线程被占满了,就要添加更多sender主机

→ 更好的方式是:跟踪空闲线程并动态调整它的数量。scalable,高可用。

如何实现它?:

semaphores 信号量:

信号量维护一组permit 许可。

检索下一条消息之前,线程必须从信号量获取许可。线程读完消息后,向信号量返回一个permit,允许另一个thread读消息

一般可以根据现存和期望消息量来动态调节permit

MS client:

消息检索完之后,需要调用metadata service获取订阅者信息。

(这时也有可能已经在frontend调用过并传过来了)但是现在才调messag小点,并且如果那时候调,temp storage就得用文档DB

获得订阅者列表之后,就可以向所有订阅者发消息了。how 发?

是否应该遍历一遍订阅者然后call everyone?

如果其中一个失败了咋办,如果其中一个很慢咋办

→ 所以不用遍历,而是选择将消息拆成任务task,每个task负责传递给一个单独的订阅者。

这样可以并行传递,并且不受bad case影响。so:

task caretor & task exector:

这俩组件负责创建和调度单独的消息任务

how 实现这俩组件?

→ 创建一个线程池,每个线程负责执行一个任务,like java里用ThreadPoolExecutor

也可以用信号量来跟踪池里可用线程,like message retriever

这样如果线程够,就直接提交处理;如果不够,就推迟或者停止处理,并把消息return给temp storage

那么就不会出现,有可能有不同的sender host接收pick up到了这个消息,而这个host刚好又没有足够线程,这种情况。现在是每个task负责把message给一个单独的订阅

现实情况下,task可能把实际work委托给其它微服务,like负责发email或者sms消息的微服务

其它可能的问题:

如何确保通知不会作为垃圾邮件发送给用户?

需要注册订阅者

每次注册新订阅者时,会 HTTP 或email发送确认消息,端点和电子邮件所有者需要确认订阅请求。

重复消息:

frondend service负责解决这个,删除重复消息

但是sender service 发消息时,有可能因为延迟导致消息重复,所以订阅者也得处理。

重试?:

重试保证了消息“至少一次”

也可以将未传递消息发给不同订阅者,或者将这类消息存在订阅者可以监视的系统中,让订阅者决定怎么处理。

通知系统可以为订阅者提供一种重试策略

保证顺序吗?

no,即使用时间戳或者序列号,消息的传递也不能保证顺序。比如过慢的消息就会过慢,重试的消息就会重试。

安全:

很重要。必须保证只有经过身份验证的人才能当发布者,只有注册的人才能当订阅者。而且一定要传准人。

用https传递,有ssl加密

存消息时也要加密

monitoring:

必须monitor每个微服务,以及端到端的客户加密

还必须让每个customer能跟踪track它们的topic的状态,like等待投递的消息数,投递失败的消息数。

which 需要和监控系统集成

总结:

可扩展吗?yes,每个组件都可以

高可用吗?yes,没有单点故障,每个组件都部署在多个数据中心

性能高吗?seesese,

前端,很快,负责的东西很小,其它都委托给异步代理了。

metadata,分布式缓存,在内存里,很快,

sender,将消息拆分成小task,可并行,快。

高可靠吗?yes,每个temp storage方案都有replica,且跨数据中心。且会重试,保证至少一次。