2016年史称直播元年,一年当中数以百计的直播应用如雨后春笋般出现在市场上,AppStore、Android市场上充斥着秀场直播、事件直播、移动直播、直播交友等形形色色的应用,占据着越来越多用户的闲暇时光。
在万众创业的大背景下,创业者们纷纷扑向这个号称数百亿的新兴市场,在这场运动中,风投、基金也携着大把的钞票为大家提供着烧钱的原料。一时间,程序猿、运营猫和产品狗们摩拳擦掌,CXO和金融巨子们也挥斥方遒。
随着直播、短视频的火爆,内容创业的激情被点燃,主播经济也迅速崛起,我们希望发挥技术成本优势,扶持有内容、有资质、有想法但无技术的创业者,和我们共同营造视频互娱的长尾生态。经过综合考虑,启动了直播SaaS项目。
这一年多我们经历了如下有意思的事情:
从无到有组织团队,
从无到有构建应用,
从无到有设计流程,
从脆弱到高并发、高可用的洗礼,
运营平台的构建,
攻击和安全,
微服务、DevOps的探索,
政策风暴的打击黑暗的摸索。
团队组织
从五月开始项目时,我们环顾四周,可用之人惟五六耳,要快速完成两月上线可用直播SaaS软件之目标可谓通天之难。
外援(内部拆借和人员外包)我们想到的第一步,这当中涉及到公司HR制度、外包单位降标供人等困难。从头到尾用了10多位外包同学,除了两位客户端和前端同学顺利转正外,被淘汰、被回收占了绝大多数。
当然,招聘是同时进行的工作,拜给力HR所赐,我们需要的几个人员在一月之内迅速到岗,同时努力劝说其他部门想离职的同学加入我们,迅速补强了研发队伍。
前端缺乏Leader,只能让外包同学担纲,甚至让优秀外包同学去面试同学(叹气),最终,跌跌撞撞坚持至第一版本发布,才迎来了前端的Leader。从这件事情上我们看出,一个团队的初期架子尤其重要,当缺乏某个角色的Leader时,就应该最高优先级去招揽过来。当然这个是一个随缘分的事情,比如,一个角色的Leader不尽人意,希望替换掉时,招聘时日漫长,拖了近三月入职,见到效果已经是四月之后。
经过一年多的摸爬滚打,我们团队规模稳步向前,团队技术氛围逐步加强,给大家营造成了相对轻松、融洽、利于成长的大环境。
总结起来:借调人员早期可用,但应早日收编或替换;外包人员早期慎用;坚持让优秀的人去面试,一流的人招一流的人,二流的人只能招到三流的人;人力问题要早发现早解决,持续招聘,保证粮草充足。
应用构建(2016)
如何构建一个App后端,与我而言绝对算是一个挑战。我之前的经历大多是一些平台的搭建和多媒体相关技术探索,诸如云转码系统、RTMP推流SDK、Push广告之类,他们都是纯后台应用。在做天天项目之前,我对Web的理解可以说停留在RestAPI阶段。
长短连接
我们首先在长短连接之间进行了一些抉择,当时的想法是对某些场景如用户心跳使用长连接,鉴于连接层的建设需要投入额外的精力,我们选择了短连接上行加IM(云厂商提供)下行的思路,这样,既能保证CS端消息请求及时触达,保证用户体验。
JSON还是PB
在服务协议抉择上,JSON是客户端喜欢的类型,而后端同学希望使用PB,考虑到前端WEB环境使用PB的困难,我们使用了前端JSON接入,后端PBRPC的架构。
PB RPC
因为之前体会过PB封装的RPC带来的好处,首先PB的编写可以取代接口文档的编写,工程师不必干重复的事情;其次,PB的转换过程中的检查可以节省一些参数检查的逻辑;其他诸如节省带宽之类的,现在看来倒没有那么重要。
因此,后端坚定不移的选择了PB,后续我们基于Beego实现了一套简单的RPC框架,利用template实现了client、controller、router等代码生成;形成了RPC接口编写规范,这套规范与grpc的规范比较接近,是grpc规范的子集。
而后,我们基于正则分析将PB文件转成了客户端工程师更愿意阅读的markdown文档,利用toc将markdown文档转换成带边栏目录的html文档,系统的文档按模块、接口、Message的层次进行目录组织,通过锚点超链实现快速跳转,再结合浏览器的文本搜索功能,工程师利用文档的效率大幅提升。
业务划分
框架不成熟,业务紧张使我们面临的大敌,所以前期我们选择了全家桶开发模式,当时的考虑是:
如果分服务,不成熟的框架修改会造成诸多服务的连带修改;
当时的设想是service层除了个别基础service复用,对其他模块的调用一律使用client来耦合;
通过全家桶+模块相互调用限制的策略,实现了起步和将来转到分服务的可能性。更近一步,我们的全家桶可以通过Client配置,实现按模块部署。
回头来看,全家桶模式有利有弊,但是适合起步阶段。优势:
1) 不必花太多时间在业务划分上,实现了业务快速起步;
2) 避免了过早的业务拆分导致的返工;半年后,随着对业务的深入理解,我们拆分了Passport、消息中心、主播、用户、房间、增值系统(订单、支付、游戏、礼物、红包、VIP等)、数值系统(计数器、数值、榜单)等诸多边界清晰的服务。
缺点:
1) 模块相互调用限制规则在具体执行过程中,由于新入职工程师的疏忽以及某些工程师的不理解而被打破;这里面暴露了流程和团队沟通方面的诸多问题,这里暂且不表。
2) 随着限制规则全家桶底层代码修改后,需要对所有的独立部署模块进行升级,否则可能导致依赖的service发生变化,但上层模块并未更新的线上问题。
3)全家桶代码量越来越大,编译越来越慢,IDE的响应也越来越慢。
4)随着后续业务的开展,基础服务(如Passport模块),需要支撑其他业务线,对稳定性的要求更高;而Passport的每次上线,因为是全家桶里面的一个模块,裹挟着其他业务模块的修改一起上线,带来了更多的风险。
IM消息和客户端版本
IM消息由服务端通过RestAPI的方式进行,但是随着客户端版本的更新,新老版本需要的消息类型不一样,造成了一些实际困难:
1) 客户端接收到不识别的消息类型,信令消息和房间消息丢弃,显示消息提示不支持;
2)消息只能增加字段,不能减少字段;
3)产品需求发生变化,发送消息需要区别新老版本,因为我们不能做到按客户端逐一发送房间消息和广播消息,因此我们设计了消息冗余发送的技术:高版本客户端忽略原消息,低版本丢弃不识别的新消息。
流程
敏捷开发团队遵循敏捷流程,但敏捷也不是没有成本的,有的团队设置了敏捷教练,但如果敏捷教练和TeamLeader各自为政,会造成效率降低,不能迅速为团队找到最佳实践。相反双方应该深入合作,敏捷教练最好向TeamLeader / CTO汇报,合理设计好敏捷流程。
经过摸索,我们整理了一套流程,并对其中一些流程制定了规范:研发接口文档规范、研发Git使用规范、提测上线邮件规范、事故CaseStudy规范,以下是流程一些要点:
需求分析和设计:按要求出设计并评审,复杂Story设置Owner
开发和测试:代码提交需要Review、开发需要维护接口自动化测试Case、提测需要发提测邮件、测试阶段,应提醒PM验收、维护两套测试环境
上线和运维:重要的上线需要发上线预告邮件,上线需要发上线通报,上线使用预览环境,必要情况选需要发数据分析和上线收益邮件,周末节假日值班,事故发生后需要CaseStudy并发送事故报告
就像注释是代码的补丁一样,流程是对自动化框架的补丁,流程最好能自动化实现,否则会造成很大的心智负担,逐步退化。举个例子:提测邮件以前均是由RD自行发出,为了方便Leader和QA同学查阅,我们对内容和标题做了规定,但是实践过程中贯彻难度较大,对于简单规则的遵守,人往往不如机器。后来QA团队开发了teamcat测试平台,对提测流程实现了自动化,研发同学只需要与WebUI交互,将更多精力放到填写提测内容和测试要点上面。测试同学和领导也可以一目了然的看到当前的当前的项目状态。
代码review的贯彻难度最大,我们经历了好几个阶段:
1)最先是由Leader自行Review,但是Leader个人精力有限,往往不能做到全覆盖;
2)由值周同学负责,这个也很难推广,同学水平参差不齐,骨干同学太忙,无法长期坚持;
3)自动化的工具如SonarQube、GoLint等工具可以帮助Leader快速掌握某位同学提交代码的质量大致情况。
测试
敏捷开发中强调测试,甚至提倡TDD来促进开发,我们经过了长期的摸索,经过了以下几个阶段:
快速撸码,野蛮生长
在这个阶段,老板意志表现比较强烈,要求两个月发布第一版可用产品,因此我们在设计上和代码上的要求无暇顾及太多,但是我们从头开始建立了自动化Case,和人工手动测试两个验证手段。后续我们发现在线下单独的单测环境中跑这些Case不能帮忙避免其他环境中出现的客户配置问题,我们便在开发环境中运行自动化Case,收到的效果是第一时间发现问题。
问题不断,迭代缓慢
测试过程中发现的BUG数量巨大,且不易收敛,每次发版旷日持久,系统脆弱,经常牵骨连筋,顾此失彼。我们开始强调研发自己维护Case,但收效其实不大,因为很多同学往往只是冒烟即可,不考虑边界,不考虑复用。往往出现大片的Case不过,测试Case腐败退化,因为排期紧任务重而更加腐败退化。
加强单测
后来随着开始微服务建设,我们重新考虑了测试的分工,研发同学注重自己单元的质量验收,通过单元测试来验证自己的代码的正确性,验证设计的可测、可扩展等合理性。要求新的微服务和新的业务模块都必须单测,前期代码覆盖率保证在60%以上,每次提交前要求单测全部通过。在经过一段时间实践后,我们又开始强调单测的整洁性和可读性,经过一系列努力,测试驱动和讲求设计的技术氛围也逐渐加强。业务架构上,会围绕着业务拆分与服务重用进行讨论,代码实现上,会围绕着设计模式和代码整洁而努力。
VSCode在Go开发方面的优势明显:图形化的Debug过程,按工程区分的环境变量,一键生成测试代码,测试覆盖评估等。选用它让我们在效率方面受益匪浅。
自动化Case
QA同学也开始了专人负责的自动化Case维护,这次,他们找到了窍门:
1)通过准备数据集等方式,保证测试的可重复运行;
2)针对不同使用方对微服务的一些限制,他们增加了不同的Case;
3)发生报警后,测试同学先定位问题,迅速找到相关研发同学进行解决。
虽然我们还没有做到传说中的代码提交后直接过测试并上线的全自动化模式,但是我们通过小米加步枪的方式,自己参照经典,结合摸索找到一条适合自己的测试自动化之路。
环境和部署
开发环境
开发阶段,我们将环境分了local和dev两个环境,
dev环境原则是上给客户端和前端同学开发使用的,后端同学不能随意修改开发环境,只能自测通过后才能Push代码,再由Jenkins任务自动更新到dev环境。
local环境用于本地开发,因为go开发本身跨平台,在Mac、Windows和Linux上均可运行,因此我们鼓励工程师在本地开发。local环境与dev环境使用相同的数据层,如果服务A需要依赖服务B,则A的local环境依赖B的dev环境即可。
测试环境
直播SaaS环境比一般AppServer的环境多一些,测试的内容也多一些,我们的线下测试环境分了以下几种:
test,用于新版本测试;
app_release: 用于新应用测试;
sandbox: 用于研发复现和验证特定版本的问题;
我们的进程分为以下三类:1)RPC Server,2)Msg Queue Consumer,3)Timer。这几个环境使用相同的数据层,通过不同环境使用不同队列实现MQConsumer的独立,最后几大环境只是共享了Timer,因此我们把Timer做得很薄,Timer只包含Timer的驱动,具体的业务通过调用RPC接口实现。通过区分几大环境,做到专有环境专有用途,多件事情并行进行。
预览环境
预览环境和线上环境使用相同的数据层,他的队列也和线上分开,除了Timer外,其他逻辑均能在线上环境做到小规模的测试。
生产环境
SaaS的灰度比较特殊,他有一个实验性质的App,线上它的新版本,我们不必部署独立的灰度环境,也能完成灰度。
高并发
高并发的核心永远是水平扩展以及缓存,水平扩展的核心是系统节点无状态,不存在单点瓶颈。这点而言,Web服务的可扩展能力还是不错的,数据层Mysql存在此类问题,但是随着微服务的拆分,我们可以将数据迁移到不同的数据库实例上实现业务拆分,而针对同一张表出现的性能问题,还能使用水平分库分表的策略进行优化,当然,随着TIBD等分布式RDS出现,也为此类问题提供了更简单的方案。
缓存问题是出现了以后能迅速加上,并且要注意加缓存正确姿势;以及具体场景的适用性,比如:用户针对自己的查询请求,为了保证数据一致性,可以不过缓存。
长连接:加强对系统资源的复用
水平扩展:必要时可以加机器解决的问题都不是问题
读写分离:读写分离,降低主库的压力
缓存:保护数据库
异步调用:削峰填谷
网关限流:保护后续服务
高可用
运维层面
预览环境做线上验证,大小上线都经过预览验证
基础框架完成事件收集,方便监控报警
利用业务日志分析完成业务层面的报警
多环境一视同仁,坚持相同标准建设监控系统
设计层面
优先考虑无状态服务(基于负载均衡) ,其次考虑主从备份,再次考虑单机冷备
重视:限流、隔离、熔断
重视:缓冲解耦(通过业务异步实现削峰填谷)
重视:供应商的多备份和监控
重视:数据层方案的梳理和监控
接口分级和校本化的降级方案(譬如第三方Token的缓存)
重视安全、攻击和接入层保护
代码流程层面
重视单测和自动化Case建设
SonarQube自动化检查
CaseStudy,同一错误不犯两变
运营和统计
服务的运营平台是个B端应用,我们最先在做模块设计时杂糅了业务和服务,我们的模块是有C端业务、B端业务、服务等。运营平台最先开始是一个MVC的典型前后端一体的后台Web服务,随着业务发展,设计和PM对产品不满意,前端介入了进来,所有的请求均通过运营平台做了一层翻译,我对此深感不满,因为任何一次改动都需要经过前端、OP、业务模块至少三个开发同学的参与。交互流程长,消息出偏差后极易导致效率下降,因此我们启动了运营平台架构的改造。首先,前后端分离不可避免,业务模块也不可能一成不变,因此唯一能固定住不变的只有运营平台,我们通过实现自动化鉴权并透传请求的方法实现了自动化的OPGateway,针对90%需求,它实现了前端和后端模块同学直接对接。而剩余10%(跨模块的查询聚合、修改聚合),运营平台单独开发自己的接口即可。
统计的实现我们采用了Spring Framework、MyBatis的Web程序,它将按天的业务统计计算出来,存储到结果数据库中,结果数据库可供运营平台查询。
驱动方式
Web接口,实现数据重新计算、结果查询等功能。
定时任务,按配置自动运行,
查询数据源
Mysql:数据的最大来源
Hive:后续我们考虑将ES和Mongo都导入到Hive体系中,方便跨库查询。
ES、Mongo、S3
结果数据库
Mysql
Mongo:文档型数据库,结果字段可自由扩展
数据仓库
随着微服务的建设,与传统的分层架构不同,数据库属于微服务的组成部分。
1)数据库被隔离的情况会越来越多,数据按服务的不同,分布在不同的数据库或不同的Mysql实例中。
2)数据库的选型更加灵活,很多Nosql数据库(如Mongo、Redis、ES等)会成为微服务数据层的首选。
由于以上的变化,数据统计变得更加困难。因此有必要创建数据仓库,将后端的业务数据归集到数据仓库中,方便数据分析。
我们实践了Sqoop+Hive的数据仓库建设的方法,通过Sqoop将RDS数据同步到Hive中,通过通过Mongo、ES的插件将数据也导入到Hive中。通过Hive查询实现业务的统计
报表系统
简单起见,我们选用了ReDash作为数据报表系统,同时我们开发了脚本,将结果定期发送给相关同学。做到数据及时可触达。
微服务(2017)
微服务是最近几年很火的一个词汇,我们从16年就希望进行微服务改造,但是限于人力与能力,迟迟没有动手,直至17年我们才开始积极探索。
业务的拆分是微服务比较关键的部分,但是微服务难点在于底层,框架和实现和自动化运维的投入和摸索。
重视Beego,我们发现他在微服务需要的负载均衡、Tracing、RPC规约、熔断等技术范畴的考虑太少,相反,grpc为大家提供了不错的选项,他基于中间件的AOP思路也可以方便的扩展框架能力。
因此,我们的架构师基于GRPC,结合我们的最佳实践,实现了一套名为Doraemon的微服务框架以及名为dolly的脚手架工具。殊途同归,与Beego、Bee不谋而合。
Doraemon提供一整套微服务开发,部署,监控的解决方案,具有如下特点:
1)基于GRPC框架对外输出RPC和RESTful接口;
2)支持多应用开发,最大程度保证服务自治;
3)提供拦截器,可以完成面向切面(AOP)编程;
4)集成基于Consul的服务治理,并提供可视化服务监控,链路跟踪;
5)集成Promethues,采集基础数据,打点监控业务情况;
框架解决了开发和部分运维的问题,但是自动化的运维仍需要投入,我们的DevOPS同学完成了如下壮举:
1)基于ELK的实时日志收集和展示体系
2)基于ES的业务报警服务,采用了Watcher和Alert双引擎
3)基于Promethues、Graphana基础报警体系
4)基于KMR的数据仓库体系
5)自研Atlas服务树服务
6)基于Jenkins+ansible的自动化部署体系
这些投入,从底层上为服务提供了强有力可自动扩展的运维保证,将我们之前预留的空白填写完成。
基于直播场景,我们抽离了如下服务的系统:
1)基础服务体系(Passport、关系服务、消息中心、配置服务、Video服务...)
2)业务服务(个人秀、房间、主播...)
3)增值系统(订单中心、第三方支付中心、游戏、礼物、红包、VIP、分账...)
4)数值系统(计数器、数值、榜单)等诸多边界清晰的服务、
原来的全家桶服务逐步演变为微服务的Gateway角色,他实现了服务的耦合,应对业务的细微变化。
服务分层沉淀除了更多的稳定的服务,他们专人负责,灵活升级,重复利用,为后续的业务发展奠定了不错的基础。
安全、攻击和黑产
网络攻击是中小平台绕不开的东西,他就像躲在暗处的忍者,在你最不情愿的时候,给你最伤的打击。
我们的客户前后经历过三四次打击,前面我们的服务一打就挂,陆续试过了运营商流量清洗、高防IP、始终没有摆脱他们的纠缠,每天付出数万的费用,颇有些破敌一百,自损三千的悲壮,偶然机会,我们想到了某商高防,接入之后,攻击立马停止。
攻击方的肉鸡在16年实现了成本大降,随之而来的是随意的兴风作浪,记得阿里在之前的世界纪录是400GBPS,而今年攻击我们的流量动辄超过600GBPS。据说某司建立了储备带宽数T的扛打机房,财大气粗,溢于言表。
打不过自然要考虑躲,我们后续设计了多域名和直播盾方案,利用了狡兔三窟以及分级降级的思想。
安全意识在云服务厂商内部还是很高的,定期安全扫描,接口全HTTPS,密码存储加盐加密,金融数据库特殊密级都是具体措施。这些问题平时不会是什么大问题,一旦出问题,技术负责人也差不多要收拾东西走人了。
网络黑产在我们平台上更加猖獗,尤以卖小片,拉主播为主。
我们后来接入了一些第三方的脏字和作弊服务,但也只解决了部分问题,举几个例子说明有多黑产人员多么有才。
有的用户,将自己的昵称改成广告语,然后疯狂加关注,我曾经看过一个用户他关注的人数达到了好几万。这里面只要转化率达到百分之一,那么他的收入就会超过万元。
总之,以上所讲的东西都是做社区、做直播等产品必须要面临的,而且要长期战斗的领域。