贝壳Go实现的多云对接存储网关建设
Posted 贝壳产品技术
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了贝壳Go实现的多云对接存储网关建设相关的知识,希望对你有一定的参考价值。
1、功能介绍
贝壳存储服务通过S3协议向业务方提供文件、图片、音视频的存储及下载。S3协议由AWS推出,在对象存储行业已经成为事实标准。腾讯云对象存储COS、阿里云对象存储OSS均兼容S3协议。
S3协议可以简单理解为一套webapi接口。通过调用接口,业务方可以进行对象数据的存取。每一个对象数据称为一个object,以一个唯一的ID来标识,object可以组织到一个bucket中,便于区分不同的业务场景。总结来说,S3就是一个用bucket组织数据,可以存储近似无限数量object的key-value(其中key为object的标识,value为存储的数据内容)存储系统。
从业务角度来说,数据需要做一些处理之后才会使用。以图片为例,业务方存储一张原图之后,实际使用时可能并不需要原图,而是根据场景进行缩放或者打水印或者格式转换之后下载使用,而且C端场景需要提高下载速度,就近下载。因此贝壳存储服务也提供了多媒体预处理以及通过CDN就近下载的功能。
贝壳存储服务稳定性可以达到4个9,存储近百亿数据,总数据量十PB级。
2、背景
存储网关作为一个底层服务,牵一发而动全身,其稳定性至关重要,初始存储网关架构如下图所示:
业务方通过S3协议上传数据到存储网关,存储网关进行验签、鉴权、文件大小及黑白名单检测之后转发到后端使用的云平台对象存储服务。作为一个存储网关,有如下两种关于大小的设置:
单对象最大大小,如果超出该限制,则拒绝传输。类似nginx client_max_body_size配置项
单对象允许使用的最大内存大小,超出该限制则将数据放置到一个临时磁盘文件后再进行转发。类似nginx client_body_buffer_size配置项
这些配置可以避免某个业务方上传文件过大影响存储网关稳定性。
任何一种架构都有其优劣势,贝壳存储网关作为一个存储统一入口,便于管理和控制,并且可屏蔽后端实际使用的云平台。因此如果有云平台迁移需求,可以做到业务方无感知,例如贝壳后端存储曾经从AWS迁移到腾讯云。
但其劣势也十分明显,任何集中式的服务都需要做好稳定性建设,防止爆炸半径过大,大面积影响服务。其次由于是统一入口,不在入口部署地域的公网上传会受限于公网网络质量,导致延时或者错误率增加。
存储网关稳定性建设涉及如下三个方面:
入口处:如果一个业务方持续上传大量的文件,占用存储网关资源或者业务方出bug,则直接影响其他业务方的使用; 验签鉴权:存储网关使用OpenIAM进行验签和鉴权,如果OpenIAM出现故障,则影响业务方使用; 后端云平台:如果后端云平台的对象存储服务出现故障,会直接影响业务方使用
也就是上图中标黄的三个部分。很不幸,2021年贝壳存储网关在如上三个方面都出现过故障。因此本文会重点介绍存储网关在稳定性方面的建设。
3、入口建设
入口处需要配置各个业务方的资源配额,防止业务方之间互相影响。除此之外,需要根据存储网关通过压测得出的最大容量做整体限制和报警,防止突增流量压垮整个系统。贝壳存储网关除了常规的QPS限制之外还增加了带宽限制。
3.1 限流
流量限制通过使用开源组件Sentinel实现,增加限流之后流程图如下:
一个请求首先是否触发bucket维度的限流,如果没有,则判断是否超过全局限流,两个限流都没有触发的话可以放行。Go语言十几行代码就可以实现一个限流方案,具体实现方法在此不再叙述,大家可参考Sentinel的实现或者网络上其他的一些案例。
3.2 限带宽
存储网关只有常规的限流并不能满足需求,大家可以想想nginx,作为一个http网关,nginx除了实现limit_req用来限制流量,还提供了limit_rate进行带宽限制。设想一个业务方流量限制为100qps,但每个请求都传输大小为10G的文件,则会将存储网关的带宽打满。
Go语言中,数据读取和写入通过Reader和Writer接口实现,其定义如下:
type Reader interface
Read(p []byte) (n int, err error)
type Writer interface
Write(p []byte) (n int, err error)
因此能够通过做一层封装,实现Reader和Writer接口,每次读取或者写入时先判断是否超过了带宽配额,如果没有触发限制,才会进行真正的上传和下载。其原理图如下:
Reader接口实现如下:
type UploadBandWidtLimitReadCloser struct
Bucket string
Payload io.ReadCloser
qpsLimiter *limit.Limiter
func (this *UploadBandWidtLimitReadCloser) Read(p []byte) (n int, err error)
// bucket维度配额检查...
// 全局配额检查...
// 如果检查不通过,则使用sentinel的匀速排队方式等待,最多等待2min...
//实际读取
return this.Payload.Read(p)
带宽限制也按bucket和全局维度进行了配额配置,除此之外,还区分了上传和下载的维度,毕竟数据首先需要保证上传成功才能够下载使用。
流量和带宽的所有限制都保存在了配置中心Apollo中,可以动态调整。其中全局维度按存储网关的容量进行常态化开启,bucket维度按业务方平时使用量的3倍常态化开启。
4、依赖降级
贝壳参考aws的IAM建设了自己的验签鉴权系统,存储网关使用该套系统进行业务方的验签和鉴权。但IAM系统也会出现故障,因此存储网关需要设计降级方案。简单来说,可以直接通过设置降级开关,放行全部请求,如下配置:
openiamDegradeSwitch = false
openiamDegradeConfig = "__default__": "allow_all"
但这样有一定的风险,本来验签和鉴权就是为了安全,不能因为稳定性完全放弃安全性,尤其是存储网关可以通过公网访问,完全放行可能导致恶意上传,损耗成本并且增加合规危险。因此我们增加了如下的一些切换策略:
可以按公网和内网分别配置降级策略,例如内网完全放行,公网只有最近12小时有成功访问记录的AK才会放行; 按bucket维度进行降级,例如只放行某些指定的bucket
5、多云切换
多云时代,如果能够对接多个云平台,不仅有利于可用性,防止单点故障,还可以通过多个供应商的引入,通过竞争降低成本。随着公司的发展或者合规性的要求,有些数据需要保存在公司自己的IDC,此时多云对接利于引入自建对象存储服务,并且数据迁移可以做到业务方无感知。
受利于S3协议成为事实上的标准,我们可以直接对接阿里云、腾讯云或者开源方案chubaofs,甚至通过协议转换可以对接其他一些分布式对象存储服务。多个云平台只需要实现下列接口就可以通过配置后切换:
type StoreRepository interface
Initiate(ctx context.Context, config *StoreConfig) (_ context.Context, err error)
// Bucket相关方法...
// Object相关方法...
CopyObject(ctx context.Context, input *s3.CopyObjectInput) (output *s3.CopyObjectOutput, err error)
DeleteObject(ctx context.Context, input *s3.DeleteObjectInput) (output *s3.DeleteObjectOutput, err error)
GetObject(ctx context.Context, input *s3.GetObjectInput) (output *s3.GetObjectOutput, err error)
HeadObject(ctx context.Context, input *s3.HeadObjectInput) (output *s3.HeadObjectOutput, err error)
ListObjects(ctx context.Context, input *s3.ListObjectsInput) (output *s3.ListObjectsOutput, err error)
ListObjectsV2(ctx context.Context, input *s3.ListObjectsV2Input) (output *s3.ListObjectsV2Output, err error)
PutObject(ctx context.Context, input *s3.PutObjectInput) (output *s3.PutObjectOutput, err error)
// 分片上传相关方法…
贝壳存储网关通过在Apollo配置中心设置开关,可以按bucket维度选择后端使用的对象存储服务。通过数据同步,还可以将关键bucket保存在多个对象存储,从而进行灾备切换。示意图如下:
云平台A故障时,可以进行读写切换。
6、监控报警
增加限流、限带宽以及依赖降级、多云切换之后,配置中心多了十几个开关。但开关还需要人来执行,因此需要相应的增加监控报警提前发现故障,并做好对应的预案。报警指标包括如下方面:
通过报警可以提前发现问题,发现之后有相应的处理手段,提高存储网关的可用性
7、新架构整体概览
存储网关整体架构图如下所示:
各部分介绍如下:
业务方通过S3协议上传下载文件、图片或者音视频;
存储服务入口处为限频模块,通过配置中心获取对应业务方的流量配额和带宽配额,配额之内的放行;
存储服务对业务方进行验签和鉴权,该模块可降级,降级策略通过配置中心控制;
协议转换模块首先通过配置中心获取该业务方对应的后端云平台存储,然后将S3协议和后端云平台协议进行转换和上传下载;
对象文件存储之后会通过消息中间件发送信息,触发多平台的同步以及多媒体处理
8、未来规划
存储网关后续会围绕着如下几个方面做建设:
平台化建设:对业务方友好,通过web平台可以进行bucket申请、bucket授权、资源转移、历史趋势观察、成本展现、流量和带宽配额展现、后端存储服务展现。也可以进行上传下载、文件分享。 边缘接入:上文介绍架构劣势时提过统一入口导致的上传局限性,跨地域上传一是会大量占用公网带宽,二是会有延迟和失败率高的问题。通过引入边缘代理节点可以加速上传。 降本增效:一是管理对象的生命周期,不常使用的可以存储到低成本服务器,二是探索多媒体处理的FAAS化,多媒体处理比较耗资源,并且高低峰明显,通过引入FAAS,不只可以满足高峰期的快速扩容,而且可以在低峰期极端缩容到0资源占用。贝壳正在进行服务的容器化建设,引入了K8S,基于此,探索多媒体处理的降本。 多媒体功能建设:图片的裁剪、缩放、水印、格式转换,音视频转码、变声、裁剪合并、缩略图,文件预览等等
9、总结
本文主要介绍了贝壳存储网关的使用场景、架构设计以及稳定性建设,其中稳定性建设是基础。除了稳定性,后续还会在易用性、功能扩充以及降本增效方面持续建设。
贝壳 Go 实现的 IM 群聊优化之路
介绍
贝壳IM为贝壳找房提供了70%以上的线上商机。为上百万的经纪人提供了快捷的线上获客渠道。日新增会话300万+。其既有互联网TO C产品的属性,又具有浓厚的房地产行业特色,是贝壳找房所属的产业互联网中重要的一环。
IM系统相比其他服务有其特殊的特点,主要包括实时性、有序性、可靠性(不重复不丢失)、一致性(多端漫游同步)、安全性。为保证这些特性的实现,良好的性能是必不可少的。本文主要阐述了针对贝壳IM单聊群聊消息的优化思路,通过压测、寻找瓶颈点、提出优化方案、验证优化方案、代码实现的多轮次迭代,最终实现了20倍以上的性能提升。
背景
2020年底的时候有业务方提出使用群聊消息进行推广活动,该场景预估300人大群需要满足100QPS的性能要求。接到需求,我们首先对群聊场景进行了摸底压测,效果为300人大群QPS为15的时候系统内消息处理就会出现积压,投递能力到达瓶颈。因此拉开了优化的序幕。
IM系统整体概览
我们首先简要介绍下贝壳IM的整体架构,一是便于理解IM消息服务难在哪,二是便于看出IM消息服务的瓶颈点在哪。
上图是IM消息发送的整体流程:
发送者通过http接口发送消息,接口处理完成写入发送队列后返回给用户成功信息。此时后台的投递服务实时从队列获取待投递消息,然后进行如下四步操作:
写入用户的收件箱
个人信箱通过Redis的zset实现,只保存用户最近800条消息,历史消息需要通过消息库查询。注意此处是一个写扩散的过程,每个用户都有一个自己的收件箱,300人的大群,任意一人发送消息,在此处都会写入群中300人的收件箱,相当于1次写入扩散为300次,因此300人大群,如果要达到300QPS,那么写扩散后在Redis层就是一个300*300=90000的QPS,记住这个数字,下文会继续提到
写入持久化的历史消息库
通过长连接服务通知接收者拉取消息
通过PUSH服务通知接收者有消息到达
用户在线可以通过长连接通知,如果用户离线,则只能通过PUSH通知
通过贝壳IM架构,可以看到消息发送到消息送达整体是一个异步服务,分为两个部分,一是接口层,二是投递服务,两者之间通过队列通信。接口层只需要写入队列即返回成功,无状态可以横向扩展,不会成为群聊的瓶颈。投递服务功能复杂并且会直接影响消息处理的及时性、可靠性。
了解过IM的同学可能会问,是否可以将写扩散模式更改为读扩散,即300人大群中发送一条消息之后,不再写入每个人的收件箱,而是只记录到一个发件箱中,读取的时候每个人去发件箱中读取。读扩散方式下,假设一个人加入了100个群,那么读取时需要将100个群的发件箱都读取一遍,而之前的写扩散模式,只需要在收件箱中按序列号读取一次即可。两者各有优劣势,通过调研发现云厂商以及微信服务群聊均是在写扩散模式下实现,证明写扩散可行,并且对贝壳IM来说,继续使用写扩散成本可控。因此在整体架构不变的情形下我们进行了一系列优化,最终也达到了预期效果。
优化措施一: 业务隔离
IM的消息按照业务属性主要分为三种:
单聊消息,即双人聊天会话,包括C2B和B2B,涉及商机获取,最核心的功能
群聊消息,即多人聊天会话, 特点是参与人数多,涉及活动推广及消息通知
公众号消息,即各种业务公众号群发消息,推送量大,已经做了隔离,并且历史消息库使用TiDB
消息投递服务负责从redis队列消费用户发送的消息,进行处理。举个例子, A/B/C三人在同一会话中, A向会话中发送一条消息, 投递服务需要保证这条消息被写入A/B/C三人各自的收件箱和历史消息库, 并通过(push/长连接)通知A/B/C。第一次摸底压测出现redis队列积压,会导致用户不能及时收到消息,这个是不可接受的。在业务侧,针对我们公司的业务场景特点,最重要的是客户和经纪人的单聊消息。从业务隔离角度考量,进行的第一个优化措施,是对单聊和群聊进行垂直拆分,分级隔离后的效果图如下:
优化措施二: 提高并发
我们继续深入投递服务的细节,看看投递服务的代码实现逻辑。总体来看,投递服务消费一条消息后,会经历两个阶段,每个阶段分别由对应的goroutine来完成, 阶段一有256个goroutine, 阶段二 有1000个goroutine,两个处理流程之间通过channel进行通信。两阶段的流程图如下:
还是以一个300人的群GroupA为例,假设发送者 SenderA发送了一个消息,阶段一获取到的消息是会话维度,即GroupA(会话ID标识)的SenderA(UCID标识)发送了一条内容为xxx的消息,阶段一的主要工作是根据会话ID获取到群内的所有成员UCID,然后将UCID哈希之后打散放到1000个channel中,此时channel中的消息是用户维度,即用户ReceiveB收到了一条内容为xxx的消息。注意阶段一此时还进行了一个扩散操作,即将每个收件人的未读数计数进行了更新操作。阶段二从channel中获取消息之后将其放入群内300个成员各自的收件箱,然后给每个人发送push以及长连接信息,通知收件人拉取消息。
可以看到阶段一是会话消息维度的处理,IO操作较少,写扩散主要有两步,一是进行一个内存channel的写入,二是以收件人维度进行了未读数的更新。阶段二是收件人维度的消息处理,并发度更大(1000个goroutine),IO操作也较多,例如写入收件人收件箱,更新会话顺序,向每个收件人都发送push通知和长连接通知。
优化措施
压测时观察到只有发送Redis队列有积压,channel队列没有积压,并且阶段二并发能力更强,考虑将阶段一中比较耗时的更新用户未读数操作放在在阶段二进行。
效果
通过将更新用户未读数放到阶段二,其实相当于增大了处理未读数的并发能力,上线后进行压测,数据为300人群,群聊消息发送可以达到75QPS,有5倍的性能提升。当300人大群发送QPS达到70时,Redis侧QPS为21000,此时IM中使用的一组缓存redis cpu被打满,单核使用率达到92%(注意redis6.0版本之前只能使用单核,因此单核cpu使用率是redis一个很重要的观察维度)。
优化措施三: 减少计算
进行第二项优化后,300人的群聊可以达到75QPS,此时一组缓存的redis实例cpu基本被打满。该组redis主要负责存储用户关系, 在投递服务阶段二发送push的业务逻辑中会大量使用。
问题
以投递一条消息到300人的大群为例,假设群为GroupA,发送者 SenderA发送了一个消息,阶段二中给每个收件人发送push的业务逻辑中存在重复获取数据的情况,例如:
SenderA的用户信息:会针对群内每个成员获取一次,即获取了300次SenderA的用户信息。
GroupA的会话信息:会针对群内每个成员获取一次,即获取了300次会话信息。
用户是否设置了免打扰:会针对群内每个成员获取一次,即获取了300次免打扰设置。
由于群聊是从单聊代码改造而来,可以看到简单重复单聊的逻辑,会造成写扩散的放大。
优化措施
针对重复获取数据,进行相应的优化措施,SenderA的用户信息和GroupA的会话信息可以提前获取一次,发送push时直接使用。用户是否设置了免打扰,原来是从用户到群组的映射关系,通过增加一个群组到用户的反向映射关系,可以通过一个群组直接获取到全部设置了免打扰的用户,减少计算量。
效果
通过合并业务数据,相当于减少了业务处理的计算量,该改进上线后进行压测,300人群, 发消息150QPS,对比优化二有了2倍的性能提升,对比初始值有10倍的性能提升。此时写入信箱QPS可以达到45k。
此时,IM使用的另一组Redis出现了告警,单核CPU使用率超出90%,并且有大量redis慢查和超时报警。通过监控发现,CPU被打满时,redis每秒建连数达到8k/s, 总连接数增长至20k,QPS为45k,通过和DBA同学沟通,这种情况下redis有一大部分cpu都被消耗在连接的建立与销毁。奇怪的是IM中使用了Redis连接池,为什么还会有大量的新建连接呢?Redis的极限QPS能够达到多少?
优化措施四: Redis连接池优化
排查过程
贝壳IM中redis库是使用连接池,目的就是为了避免新建销毁连接带来的消耗。目前使用的redis库比较老,结合之前线上存在偶发的redis慢查报错,首先怀疑是连接池的实现问题。
验证
为了方便在测试环境模拟复现redis大量建连问题,开发了一个模拟投递程序可以指定发消息qps和参与会话人数。
如下是im使用的redis库(简写为imredis)与业界比较成熟的goredis库数据对比
通过测试数据发现如下三点:
在发送端qps相同的情况下,如果没有大量的新建连接,则redis CPU使用率会大幅下降
发送端qps到达120时,使用goredis,此时CPU使用率93%,接近打满,redis qps 144k。(注意此处的模拟程序只使用了一个redis实例。IM线上服务首先按功能分组,每组Redis一般是4个实例起步。分组和分实例的逻辑都在imredis代码中实现)
通过模拟测试,可以证明imredis库连接池实现确实有问题,并且验证出redis在我们的场景下极限QPS可以达到140k。因此通过改造连接池,可以达到优化效果。
连接池缺陷
// Get retrieves an available redis client. If there are none available it will
// create a new one on the fly
func (p *Pool) Get() (*redis.Client, error)
select
case conn := <-p.pool:
return conn, nil
default:
return p.df(p.Network, p.Addr, p.Auth, p.Db, p.Timeout)
通过查看imredis连接池代码,发现虽然初始化时会指定最大连接数量,但当QPS升高并且接池中获取不到连接时,会新建连接进行处理。
优化措施
参考goredis和其他连接池实现, 采取以下优化措施:
备用连接池。大小是默认连接池的10倍,如果默认连接池满了, 连接会暂时存放在备用连接池,每过固定时间对备用连接池内的连接进行释放。避免在大量并发时频繁新建和关闭连接。
对新建连接通过令牌桶进行限制,避免短时间内无限制大量建立连接,导致拖垮服务。
最终效果
压测验证300人大群可以达到320qps左右。投递能力较优化三有两倍的性能提升,较去年年底有20倍的性能提升
总结
群聊300人大群达到320qps,已经能够满足未来两年内的业务需求。并且随着群聊的优化,尤其是Redis连接池的优化,单聊的性能也有了显著提升,从年初的2000达到了12000。未来如果有继续提高性能的需求,只需要对redis进行双倍扩容,并且将投递服务也进行扩容即可。
通过本次优化,可以看到,大部分的性能优化只需要进行代码层面的改造,通过压测找到瓶颈点,摸清整个链路的短板并改造这个短板。此时QPS就只是一个数值。需要更高的QPS时只需要扩容、扩容、再扩容即可。
推荐阅读
golang源码阅读:livego直播系统
以上是关于贝壳Go实现的多云对接存储网关建设的主要内容,如果未能解决你的问题,请参考以下文章