system design 俄罗斯大叔系列
消息系统
场景:需要发送消息以响应某些事件
like:信用卡交易金额超过限额时,需要通知持卡人
服务监控系统遇到大量API产生的故障,需要通知on-call工程师
抽象起来就是:一个publisher 生产消息,并传递给订阅者Subscribers
当发布者以某种顺序调用每个订阅者并等待响应时,我们可以在发布者和订阅者之间建立同步通信
难点:当订阅者和消息的数量增长时,系统难以扩展,并且难以扩展以支持不同类型的订阅者。
so,我们可以引入一个新系统,该系统可以注册任意数量的发布者和订阅者,并协调它们之间的消息传递
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
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
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:
一个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共享数据
一致性协议:
- 单主:2PC, Paxos, Raft
- 多主:Gossip (Redis)
接下来request来到temp storage,为什么叫temp,因为message停留时间很短。
Temporary Storage
- 必须快,高可用,可扩展,
- 也需要持久性,如果订阅者不可用,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
(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,且跨数据中心。且会重试,保证至少一次。