拥抱云原生——微服务架构项目的设计与实现(以Simple-Douyin为例)
技术选型
在技术选型阶段,我们就敲定了本次项目的Baseline:拥抱云原生,并在后续的过程中也尽力去落实了这一点。
从该项目的API文档可以看出,其整体架构并不繁杂,大而化之的讲是三个主要的功能逻辑模块,分别是用户与视频这两大APP的基本功能,而后在此基础上衍生出,点赞与评论的视频域功能,关注与聊天的用户域功能。但是从细节来讲,不同模块之间的牵涉较多。
举例而言,从【查看当前播放视频的用户页】这一功能点来说,其需要涵盖用户的基本信息获取,用户所发布以及点赞的视频获取(这其中又包含三个环节,一是获取存在对应关系的视频id,二是在视频id的基础上完成视频信息的获取,进一步的由于视频信息中存在作者这一用户信息,也需要进行获取与补充)。单单这一功能,就牵扯到至少User,Publish,Favorite三个服务之间的所辖功能的信息交互。
由于抖音作为国民级互联网应用,具有极高的用户广度与深度,使用体验极大耦合用户的使用时间与存留率,因此确保功能服务可以稳定,可靠是我们在开发这一项目时的关键要求。同时,由于以上功能耦合的特点,我们还希望不同的模块之间可以彼此独立,各司其职也就是常说的【只做一件事,把一件事做好】 。在确保具有较好的后期可维护性以及功能延展性的同时,也将某一服务崩毁时的影响降至最低
因此,基于以上特点,我们选择了微服务架构体系进行项目的开发,主要是为了能够有效利用以下三个特性:
-
模块化:这使得应用程序更容易理解、开发、测试,并变得更能抵御架构退化。
-
可伸缩性:由于微服务是独立于其他服务实现和部署的,它们可以独立地进行监控和伸缩。
-
集成异构和遗留系统:微服务被认为是现代化现有单体软件应用的有效手段。
-
分布式开发:使团队开发者之间可以有效协作,将彼此之间的影响降至最低。
在确定下微服务架构之后,与之相关的数据库服务也随之确定了下来。关系数据库使用Postgres,由于其后发的优势,以及其逐年演进带来的益处,我们认为Postgres已经是一种趋势。同时使用redis作为热点数据的缓存组合。
应用的核心架构我们选择了字节跳动所开源的cloudwego的hertz作为api网关,kitex作为微服务端,二者联合使用在多数情况已经被证明是最佳实践。微服务端的服务注册中心使用了etcd
在应用监控部分,我们选择了经典的Prometheus做数据采集与存储+Grafana做数据查询与可视化这一经典组合。
最终,我们将使用到的全部技术陈列于下:
- Postgres www.postgresql.org/
- Redis redis.io/
- Kitex www.cloudwego.io/docs/kitex/
- Hertz www.cloudwego.io/docs/hertz/
- etcd etcd.io/
- Prometheus prometheus.io/
- Grafana grafana.com/
- Docker/docker-compose www.docker.com/
架构设计
服务边界划分
-
Hertz作为API网关,承接所有http请求的入口,并完成参数绑定与校验等功能
-
Kitex作为微服务,承接服务的实际逻辑处理
- User服务,基础功能接口,提供与用户相关的基础功能
- Feed服务,基础功能接口,提供与视频推流相关的基础功能
- Publish服务,基础功能接口,提供与视频发布相关的基础功能
- Favorite服务,视频附加功能接口,提供与视频点赞相关的功能
- Comment服务,视频附加功能接口,提供与视频评论相关的功能
- Relation服务,用户附加功能接口,提供与用户关注相关的功能
- Message服务,用户附加功能接口,提供与用户间消息相关的功能
-
具体微服务所提供并暴露的RPC服务如下表所示
| 服务名称 | 用途 | 依赖服务 | 是否仅内部调用 |
|---|---|---|---|
| UserRegister | 提供用户注册服务 | 无 | 否 |
| UserLogin | 提供用户登录服务 | 无 | 否 |
| UserInfo | 提供用户信息获取服务 | PublishWorkCount,UserFavorCount,UserFavoredCount,RelationFollowCount,RelationFollowerCount,RelationIsFollow | 否 |
| Feed | 提供视频流获取服务 | PublishVideoInfo | 否 |
| PublishAction | 提供视频上传发布服务 | 无 | 否 |
| PublishList | 提供用户视频上传列表服务 | PublishVideoInfo,UserInfo | 否 |
| PublishVideoInfo | 提供视频信息获取服务 | VideoFavoredCount,FavoriteIsFavor,UserInfo | 是 |
| PublishWorkCount | 提供用户上传视频数查询服务 | 无 | 是 |
| FavoriteAddAction | 提供视频点赞功能 | 无 | 否 |
| FavoriteDelAction | 提供视频取消赞功能 | 无 | 否 |
| FavoriteList | 提供喜欢视频列表获取功能 | PublishVideoInfo | 否 |
| UserFavorCount | 提供用户点赞数查询功能 | 无 | 是 |
| VideoFavoredCount | 提供视频被点赞数查询功能 | 无 | 是 |
| UserFavoredCount | 提供用户被点赞数查询功能 | 无 | 是 |
| FavoriteIsFavor | 提供用户与视频之间点赞状态查询功能 | 无 | 是 |
| CommentAddAction | 提供视频评论功能 | 无 | 否 |
| CommentDelAction | 提供视频评论删除功能 | 无 | 否 |
| CommentList | 提供视频评论列表功能 | UserInfo | 否 |
| CommentCount | 提供视频被评论数查询功能 | 无 | 是 |
| RelationAdd | 提供用户关注功能 | 无 | 否 |
| RelationRemove | 提供用户取消关注功能 | 无 | 否 |
| RelationFollowList | 提供用户关注列表功能 | UserInfo | 否 |
| RelationFollowerList | 提供用户粉丝列表功能 | UserInfo | 否 |
| RelationFriendList | 提供用户消息列表功能 | UserInfo,MessageGetUserLastMessage | 否 |
| RelationFollowCount | 提供用户关注数查询功能 | 无 | 是 |
| RelationFollowerCount | 提供用户粉丝数查询功能 | 无 | 是 |
| RelationIsFollow | 提供用户之间关注状态查询功能 | 无 | 是 |
| MessageChat | 提供用户间聊天消息拉取功能 | 无 | 否 |
| MessageSend | 提供聊天消息发送功能 | 无 | 否 |
| MessageGetUserLastMessage | 提供获取用户间最后一条聊天消息的功能 | 无 | 是 |
数据边界划分
在RPC服务对照表中可以看出,除了项目要求的服务接口以外,我们还设计了其他一系列用于辅助接口间逻辑解耦与数据解耦的接口,这是由于我们希望在该项目中,服务边界与数据边界可以清晰简明。各个微服务做好各自数据域领域范畴内的事,而不可侵限其他服务接口范畴。
譬如:在本项目中,将与业务相关的所有计数信息(如点赞数,关注数等这类基于count语句的查询功能)的维护均下放至对应管理该数据表的微服务处,而并没有为了便利起见将数据拢括在一处进行查询,各个微服务仅有自己对应关系数据库表的访问权限,也仅有对应业务的redis数据库的访问权限。虽然这在一定程度上增加了架构设计了复杂程度,也可能在一定程度上影响了性能,但这样的设计明确了服务之间的数据边界,最大程度上避免了随着项目的后续开发引起的项目架构劣化带来的可维护性降级。这也是我们精心设计架构的初衷。
| 微服务 | 所管辖pgsql数据表 | 所管辖Redis数据库 |
|---|---|---|
| User | Users | UserRDB |
| Feed | Videos | |
| Publish | Videos | PublishRDB |
| Favorite | Favorites | FavoriteRDB |
| Comment | Comments | |
| Relation | Relations | RelationRDB |
| Message | Messages |
架构设计图
Redis缓存设计
中我们已经介绍了在不同微服务之间的数据流与接口调用,不难发现其中对部分信息的访问较为高频,并且是读多写少的数据范畴,可以使用Redis以降低对Postgres的负载压力。
| DB | Key | Value | 备注 |
|---|---|---|---|
| UserRDB | user_id | common.User | 其中并不包含任何计数器信息与关注状态,需要额外获取 |
| VideoRDB | video_id | common.Video | 其中并不包含任何计数器信息与点赞状态,需要额外获取 |
| UserCounterRDB | user_id | UserCounter | 其中包含用户的点赞数与被点赞数 |
| VideoCounterRDB | video_id | VideoCounter | 其中包含视频的被点赞数与评论数 |
| RelationCounterRDB | user_id | RelationCounter | 其中包含用户的关注数与粉丝数 |
| RelationRDB | user_id to_user_id | Boolean | 用于查询用户与用户之间的关注状态 |
| FavoriteRDB | user_id video_id | Boolean | 用于查询用户与视频之间的点赞状态 |
有关Redis的缓存设计思路与分析,在接下来中会详细介绍,这里不再重复。
基于场景的关键设计节点分析
| 序号 | 场景问题描述 | 采取的解决方案 |
|---|---|---|
| 1 | 在该项目的数据域中存在大量的计数字段,如点赞数,关注数等等。在查询时,计数字段需要使用count语句,而该操作相对耗时,不可能频繁进行。 | 将所有的count操作的结果进行缓存。对应于UserCounterRDB,VideoCounterRDB以及RelationCounterRDB。这三个RedisDB中将会存放必要的计数器信息。业务逻辑中需要用到计数器时将会从Redis中尝试获取,如果其并不在Redis中,则会前往Postgres中查询,但这一操作仅会进行一次。之后再对相关计数器产生变更时,不仅会在Postgres中进行对应实体的更新操作,也会随之更新Redis中的计数器信息 |
| 2 | 在请求User或Video对象信息时,其中有计数器信息与IsFavor与IsFollow字段信息。后两者信息与当前请求发起者呈动态相关,频繁前往Postgres获取代价较大。 | 计数器信息已经在场景1中得到了解决。IsFavor与IsFollow字段信息分别对应两个RedisDB进行存储。与之前类似的业务逻辑,当需要获取关系时会首先前往将会从Redis中尝试获取,如果不在Redis中则会进行实际查询并更新至缓存内,这一操作仅会进行一次。 |
| 3 | 部分信息与Redis紧耦合,由于Postgres与Redis之间存在缓存一致性问题,在高并发场景下可能会出现返回结果非预期的情形。 | 两种策略:1:数据一致性要求高:延迟双删;2:性能要求高:不做处理,一定程度上牺牲数据一致性以确保并发能力,在APP端通过刷新操作获取新结果在这里,由于我们并没有APP端的控制权限,因此采用了延迟双删策略 |