架构设计

58同城云认证架构设计

2020-12-23  本文已影响0人  王勇1024

前言

2016年7月至2018年9月,我曾有幸在58同城工作了两年多的时间,负责认证相关业务。在两年多的时间里,从最初只有我一个人,发展到后来16个人的云认证团队,我们也实现了实名认证量近10倍的增长量,辅助上游业务消灭了大量的虚假信息,也为用户提供了更多的安全保障。

实名认证量变化曲线

在那两年里,我主导设计和开发了云认证、实名库、人脸认证、微信认证等一系列意义重大的项目,都取得了非常不错的成绩,现在回忆起来,自豪之情依然会油然而生。
下面我重点介绍一下云认证项目。

背景

在58集团实现全站实名制是我们云认证团队的战略目标,但随着全站实名制的逐步推进,认证量和服务调用量迅速增加,原来的服务架构暴露出的问题越来越多,主要体现在以下三个方面:
1.服务之间严重耦合
2.服务性能低下
3.服务稳定性差
所以我们设计和开发了云认证项目,下面我们看一下云认证的整体业务架构

一、架构设计

我先从宏观上对云认证的任务架构和技术架构做一下介绍。

1.1 业务架构

业务架构

云认证支持58集团各个平台的接入,为了方便各平台接入,我们提供了4种认证产品,可以支持PC/M/APP三端各种认证方式的快速接入。
目前(2018年5月)我们支持7种实名认证和4种企业认证,以及150多种其他认证,我们是业内提供认证方式最多、最全面的企业。
另外,我们还提供了完善的认证统计、认证查询、认证审核功能
所有的认证行为,都会经过风险控制层进行风险识别,风险控制层包含虚假模板、虚假人脸、OCR识别等47种策略,可以有效拦截虚假认证。
最终,数据会进入基础服务,由基础服务对外提供服务能力
并且,在整个过程中,我们实现了服务立体化监控和完善的日志记录

1.2 技术架构

下面是云认证的技术架构:

技术架构

接入层负责对接各个业务线的接入,并通过HTTP或SCFV3协议与认证服务层进行交互。
认证服务层我们采用了微服务架构风格 ,按照认证方式和功能划分服务的粒度,使每个服务都可以独立开发、独立维护、独立扩展。认证服务之间完全解耦,只与对应的基础服务打交道,这使得原本复杂的网状结构变成了简单的星型结构认证中心对外提供统一的服务接口,使我们实现了对接口的统一管理,也在很大程度上节省了调用方的接入成本。
基础服务层,我们对数据水平切分,将认证状态与认证详细信息分别存储在Wlist和MySQL中,即保证了系统的高性能,又可以满足审核后台复杂的查询需求。通过引入基础服务层,使得我们实现了业务逻辑与数据存储的解耦,从而使业务扩展更加灵活。

二、问题和挑战

在开发云认证的过程中,我们遇到的关键问题是,由于云认证是一个I/O密集型服务,存储方案的选择对性能至关重要,如何选择合适的存储方案?
再有,针对一些必须要查询MySQL的请求,在使用缓存的基础上,如何提升缓存的命中率?
第三,如何对服务进行性能调优,消除系统瓶颈和潜在缺陷,使服务能够高效稳定运行?

问题和挑战

2.1 存储方案

上面是我们对存储方案的要求,下面的我们考虑过的几种存储方案,我们看到,MySQL/MongoDB/Redis均有几项指标不能满足我们的需求,而Wlist各项指标均能满足需求,并且我们要存储的也是“一个用户多种认证”的Key-List结构数据,所以我们最终选择的WList(WList是58同城内部使用的Key-List结构数据存储方案)。

数据库选择

2.2 缓存命中率

为了提高缓存命中率,我首先抓取了线上一个小时的请求,然后对这些请求进行分析,发现69%的数据都是在最近一周内被更新过的,并且数据的访问频率呈现指数分布,少部分数据被频繁访问。

热数据分布

因此,我采取的策略是,在Redis中维护一张热数据排行榜,在系统启动时,通过定时任务,按照数据热度和最近更新时间预加载20万条数据到缓存中
最终,我们只预热了1%的数据,就使缓存命中率达到了67.4%

缓存预热

2.3 性能调优

性能调优分为三个阶段:

2.3.1 基准测试

我们期望的性能是在并发线程数为100的情况下,单机QPS能达到4000次,客户端平均耗时不超过50ms,每分钟超时量超时量小于5次。下面是我们测试的结果

测试报告
2.3.2 负载测试

第二个阶段,我们逐渐加大了并发线程数,发现前期服务性能有所提升,但并发线程数超过140以后,服务的性能开始逐渐下降,并且开始出现大量超时。同时,在服务一侧也观察到服务的全部耗时远大于执行耗时。

负载测试效果 访问量耗时

通过SCF的线程模型我们知道,请求从进入队列,到被工作线程处理完成的时间就是全部耗时,工作线程从队列中取出这个请求,到处理完成的时间就是执行耗时。两者只差就是请求在队列中等待的时间,这是种典型的生产者过多,而消费者过少的情况。

SCF线程模型

所以,要想提升系统性能,就需要增加工作线程数量,也就是增加队列数量
随后,我们开始逐步增加队列数量,来观察服务分别的100/150/200并发下的负载情况,从图上我们可以看出,在队列数量为128时,服务性能达到最佳,单机QPS达到8600次,超时量为0。

image.png
2.3.3 稳定性测试

在稳定性测试阶段,我们本来是计划测试一周时间,但到第二天,我们就观察到服务开始出现明显的“毛刺”现象。

我们再观察系统的其他各项指标,发现这些“毛刺”出现的时间和Full GC时间基本吻合,由此,可以基本确定是Full GC导致的服务性能下降。

老年代内存变化情况 GC状态统计

根据经验,出现比较频繁的Full GC,一般来说,有两种情况,一是代码编写有问题,导致了内存泄露,二是JVM参数配置不合理。
于是我首先对代码进行了CodeReview,确定代码并没有问题。于是,我导出来JVM的配置参数。

调整前JVM参数配置

这是我摘录的部分参数,上面几个参数分别设置了堆内存大小,年轻代大小,并发收集器线程数,老年代使用了CMS并发收集器,这些都没有问题。下面三个参数是SCF默认的配置。
通过这三个参数我们可以看到,它去掉了Survivor区,让新生代中在一次Young GC后存活下来的对象直接进入老年代。这其实也是我们偶尔会用到的一种配置方式,主要是为了解决promotion failed问题,所谓的promotion failed问题,就是新生代有大量对象需要进入老年代,而老年代空间不足,就需要触发一次或者连续触发多次Full GC,造成服务较长时间停顿。
这里采用的解决方案就是,去掉Survivor区,并通过CMSInitiatingOccupancyFraction控制老年代的GC时机,保证老年代中总是有足够的空间来容纳新生代中存活下来的对象,但这种解决方案的隐患就是会导致Full GC比较频繁。
我们的服务对性能很敏感,即便老年代采用了并发回收器,也难以避免频繁的Full GC对性能的影响

我最终采用的解决方案是,增大Survivor区和进入老年代的年龄,让绝大多数对象在新生代就被回收掉,同时调整触发Full GC的时机,保证老年代中始终有足够的空间。这样既减少了Full GC的频率,又解决了promotion failed问题。

调整后的JVM参数配置

三、优化效果

这是我截取的最近一周(2018年5月数据)服务的处理耗时情况,可以看到服务非常的稳定:

请求耗时情况

经过一番辛苦努力,我们最终达成了一系列明显的成果:

项目效果

截止到2018年5月,认证中心服务的调用方已经超过160个,单日调用量达到17.6亿次(2018年5月数据),在春节期间,服务连续运行1个月没有重启,依然很稳定。

四、总结

在58工作的两年多里,通过担任认证技术负责人,负责项目,和团队内及业务线的同学沟通需求,合作开发,我的团队合作能力、沟通能力都有明显提高。
在整个过程中,遇到了各种各样的技术问题,我认识到自己的技术能力还有很大提升空间,所以我坚持每天学习,并且将自己的学习成果以技术分享的形式分享给团队的成员,让大家都有所进步。非常感谢58给我提供的平台,感谢曾经和我一起并肩作战的小伙伴们,即便已经离开了,依然非常感谢!

上一篇下一篇

猜你喜欢

热点阅读