新闻客户端信息流重构系统设计——从零到一
项目背景
接口调研及排期规划
新闻客户端旧版imcp接口相对较多,重构时采用先开发核心接口(访问量大的接口),再逐步迁移非核心接口。
接口优先级排序
根据线上一段时间的access日志进行统计,按访问量排序结果如下:
awk '{print $7}' access.log|sort | uniq -c |sort -nk 1 -r|head -30
10354 HEAD /httpchk.php HTTP/1.1
6518 GET /client_base_config
5213 GET /ifengNewsRedidentPush
3713 GET /userBehaviorRecord
3081 GET /ifengNewsKeepLiveConfig
2944 GET /commentEmojiConfig HTTP/1.1
2064 POST /nlist
1939 GET /thirdoutput
1680 POST /getNewsRecList HTTP/1.1
1190 POST /NewRelatedDocs
1001 GET /api_phoenixtv_details
944 GET /weishare_token_api
826 POST /getNewsDocs
734 GET /NewRelativeVideoList
658 GET /its HTTP/1.1
634 GET /client_search_hotword
619 GET /android_api
568 GET /totalProfile
513 GET /api_vampire_article_detail
481 HEAD /runaccess/android
385 GET /phoneweather
361 GET /ipadtestdoc
327 GET /userInterestTag
324 GET /interest_select
321 GET /ClientNews
313 GET /share_zmt_home
302 GET /api_wemedia_index
263 GET /client_share_bottom_number
238 GET /burroughsnews
209 GET /shareNews
176 GET /nlist
167 POST /irecommendList
165 POST /NewRelativeVideoList
142 GET /ipLocation
128 GET /favicon.ico HTTP/1.1
126 GET /newH5Weather
118 GET /awakeIfengNews HTTP/1.1
115 GET /client_share_news_recommend_test
111 POST /ClientNews
110 POST /ipadtestdoc HTTP/1.1
根据业务分析,决定一期先重构如下几个接口
信息流列表:nlist
客户端正文:getNewsDocs/ipadtestdoc
文章相关:NewRelatedDocs
视频正文:api_phoenixtv_details
频道配置:totalProfile
视频相关:NewRelativeVideoList
客户端配置:client_base_config
专题正文:TopicApiForCmpp
人员安排,小组分工
人员安排
架构师 1人
业务开发组 2人
调优监控组 1人
Swoole服务组 3人
配置组 1人
业务组分工
接口范围
列表1个 正文3个
人员分工
列表:王爵, 正文:沐杉
排期细化
(共20天,排除其他工作任务的情况下):
1.流程梳理期(共同参与) 列表3天+正文3天 共6天
1.1细化接口流程
1.2梳理与主业务无关的逻辑
1.3梳理核实旧版兼容逻辑
2.数据源协议对接,确认一次请求和二次请求 2天
3.模块拆分设计,版本兼容设计 ,广告模块置入 4天
4.输入输出参数优化,返回字段优化 1天
5.接口开发,框架优化,测试联调 7天
6.相关文档整理-待定
日常开发制度
为保证开发进度和开发质量,不出现重大问题,在重构期间制定了相关制定
编码规范
新加入的团队成员初次合作,为保证编码风格统一,统一制定了php编码规范以及IDE的代码格式化规范,以统一成员间的代码风格
周例会
为了跟踪和梳理遇到的问题,每周固定时间(大概半小时)统一组织周例会,会上把大家遇到的问题和需要讨论的技术方案进行陈述,大家共同分析或者由架构师给出方案建议。同时制定出下一周(下一阶段)的重点工作。
周例会中要强调“里程碑”,即一段时间后的重要节点,需要实现哪些重要的功能模块,给大家信心。
code review
定期组织代码审查会,把大家的代码进行审查和讨论,确定优化方案
问题梳理
在重构初期,由于之前的业务逻辑复杂,因此自己花了很长时间去理解之前的逻辑,并画了流程图。
同时,在开发新接口前,还需要向以前负责接口的同事咨询一些业务细节,如下是当时询问的问题列表:
本次开发基于全新版本,不用考虑历史逻辑,但要考虑后续的版本差异
整理头条和正文、其他信息流哪些是公用的模块
梳理流程中无关的(不做的)流程(如行为上报,push弹窗等) 梳理后去掉
梳理哪些逻辑是废弃逻辑,本次重构不再考虑的 列表,找相关人核实,去掉
整理接口输入参数,返回参数中已废弃的参数,优化输入输出结构 整理 优化
广告模块后期置入,基于原类微调改造 可暂不考虑优化
新数据源的提供形式,分类,数据结构(旧版mongo中的数据被什么所取代?) 直接对接数据层
确认哪些流程和数据是数据源直接可以提供的,保证一次请求可能拿到全部数据 直接对接数据层,或者二次发起请求
======信息流其它疑问及建议======
程序设计建议:
5.推荐接口调用前,编辑数据固定条数 待定
3个引擎,1个头条,1个上下拉,1个非上下拉 ok
nlist page和action模式 二选一 ok
chconfig从rule中抽离,在配置中心单独成立配置项 ok
rule里的下拉不展示(downNoShow)独立成频道规则 ok
cmpppod 和 oldpod获取数据源后的格式化操作(统一字段名) 整合成一套 ok
aqudata中,2320中ids中有list的和没有的 统一成一个 ok
小列表递归,返回值list ok
专题正文会调信息流里的 getTopicTreeNews 专题套专题 ok
接口鉴权保持目前一致 ok
参数预处理处理频道号 ok
财经头条接口下线,自媒体商品信息不需要考虑 ok
cmpp数据全部获取过来后,解析实例化的性能开销,实际只用1条数据
重构设计疑问(需要确认的):
列表频道id参数多个变1个,由配置中心统一处理,(旧版id顺序会影响返回结果)
上划每次都返回置顶top数据,为了兼容客户端展示用,改版后建议客户端修改
列表整体结构是否缓存(内容组织层)
评论数的获取能否在数据源一次返回,不做二次请求
推荐接口的focus内容 能否单独给,不混在流数据中
rule里推荐位的多个数据源 是否能整合成1个?
列表是否支持其他平台的调用?(非客户端,其他平台可能传其他参数,单独返回定制化数据)
推荐接口返回的specialParam字段能否给成json对象
推荐返回的other字段中,包含很多信息,如缩略图,给过来是半成品,能否推荐直接对应到相应的字段中去,或者能否推荐只给id,其他信息我们自己查
焦点图先从推荐给的数据中分离,再合并到顶部,再分离出列表?其他频道?
视频信息调接口,二次请求,能否一次获得
财经频道数据源接口特殊接入 后续能否不支持列表外的数据源
横排内的子元素都要走外面的大流程 循环执行 性能低(取自媒体号)
正文文章id位数能否统一
正文的数据源能否统一
为方便开发和测试,需要一份包含目前会用到的全部模块组成的一个信息流,全流程覆盖的,可以新旧版本对比的一套数据源(推荐位和上下拉模式各一种)。
实现机制理解:
rule里的index为第一页的pageSize,other为非首页的pageSize
推荐数据与编辑数据的混合机制,类似广告
二级导航rule,otherrule代表模块样式
问题汇总:
正文:
正文内容属性整合,和广告信息,相关信息并列,格式见wiki:http://know.mnews.ifeng.com/pages/viewpage.action?pageId=15508510
推荐:
推荐给的docid都统一成ucms id? x
确认推荐给的docType在用的都有哪些列表? ok
头条的前2屏取default数据,为何需要将pullup强转为default ok去掉逻辑
推荐接口的focus内容 能否单独给,不混在流数据中 可以提供
推荐接口返回的specialParam字段能否给成json对象 保持
推荐返回的other字段中,包含很多信息,如缩略图,给过来是半成品,能否推荐直接对应到相应的字段中去,或者能否推荐只给id,其他信息我们自己查
lastdoc 上报机制确认
内容方面的疑问:
1.评论数的获取能否在数据源一次返回,不做二次请求
2.推荐位的多个数据源 是否能整合成1个
3.列表的视频信息调接口,二次请求,能否一次获得
4.财经频道数据源接口特殊接入 后续能否不支持列表外的数据源
5.获取自媒体信息随列表一并给出(包括各种号信息)
6.正文的数据源能否统一(如统一到ucms)
7.文章的阅读数和评论数从基础数据直接给到
8.数据接口支持批量获取
关于测试需求:
特殊用户群体,特殊地域情况,头条频道特殊业务逻辑验证,
新旧接口降级切换流程 ,业务模块降级流程 业务方面暂时补充这些
技术方案
demo讨论
由于重构项目从0开始,框架的使用和核心流程的设计需要确定一套方案。当时采用的是3个团队成员分别根据自己对项目的理解,写一套demo,之后开会介绍自己demo实现的思路和想法。
最终通过讨论,自己设计的这套开发框架得到了认可和执行,最终的方案以此demo为基准。
框架调研
最初打算用php的 Laravel框架来进行开发,后来考虑到该框架较重,不适合前端API的性能,因此从Laravel框架中抽离了几个核心组件(路由,自动加载)进行了自定义框架的配置
正式开发
业务组和服务组基于调研后的框架进行开发
开发模式采用 敏捷开发+螺旋模式+里程碑 的混合模式
php7 与 swoole
框架方案确定后,整体的业务开发方案也基本确定。模式为:
web -> swoole -> 第三方API
之所以在web和第三方api间加入一个swoole层,目的是:
- 逻辑清晰,将请求数据部分单独成立一个项目,便于管理
- 重复利用swoole的并发协程机制,提高请求效率
- 更好更专注的将监控,熔断,降级等策略应用到请求第三方接口上
重构项目使用了php7+swoole+docker的开发模式,与之前php+fpm的模式不同
性能调优
随着重构功能开发的进行,性能的问题也在逐步显现出来,因此对如下几个方面进行了优化
空跑框架
新项目由于使用了自研框架(路由部分及自动加载部分取自了laravel的相关组件),框架本身会有一些性能损耗,因此进行了空框架的性能测试,测试输出hello world
压力测试 ab
对业务相对复杂的头条接口进行了ab压力测试
第三方服务化(tars)
针对之前的http接口,获取内容接口进行了服务化改造,新的接口使用tcp服务,效率提升了很高,响应时间控制在了几十毫秒
批量接口
由于web端的“批量预请求结果缓存机制”,需要将批量的请求单独组合成数组的方式发给Swoole(而不能用手动拼接逗号分隔的形式),需要Swoole端进行参数转换,转换后发送一次请求(之前的处理方式是发送多次协程来请求,效率较低)
xhprof性能查看
在测试环境部署了一套xhprof,可以查看调用的耗时,便于排查问题
调试debug入口
在url中增加debug参数,可以打印出本次请求Swoole的耗时,参数,返回值以及真实调用第三方的url,便于排查问题。
同时,debug模式会使缓存失效,更方便跟踪过程
客户端对接,历史遗留问题解决
历史遗留问题主要在数据源推荐处,当时约定的一些字段有的已经废弃,有的功能重复等
头条推荐接口优化
1.焦点图分离,考虑与documentList同级新增字段返回
2.others 里面的 url=http://api.iclient.ifeng.com/ipadtestdoc?aid=ucms_7oF3QfFDYTA,拆分出aid字段
3.specialParam和ext->specialParam统一,值为json字符串
4.编辑固定位置条数先获取,再计算size。编辑固定数据能否推荐统一出,包括列表和焦点图。
5.确认头条推荐接口外层的字段都有 flagMap,documentList 字段
6.if($jsonData['flagMap']['flag_jpType'] == 'jpFilter') => 丢弃编辑固定位数据,确认该逻辑是否还存在,和size参数的逻辑冲突
7.前2次上拉,pullup改为default,推荐接口已实现?
8.头条接口本次设计的字段调整,在其他频道推荐接口同步更新
9.提供各模块测试数据,之前只提供了一两个测试的
客户端:
列表,正文url会变,是否有影响?ok
新输入返回参数格式见wiki:http://know.mnews.ifeng.com/pages/viewpage.action?pageId=15508171
列表:
移除push弹框,登录弹框,绑定手机弹框,事件上报相关功能ok
输入参数删除:ok
installTime //安装时间 时间戳
openNum //打开次数
pushStatus //push 打开状态 1/0
closeWinType //关闭的窗口类型
closeWinTime //关闭的窗口时间
closeWinCount //关闭的窗口次数
返回参数确认:
syRetainOldNew 下拉后清除历史数据,保留条数
downHideNews 下拉后自动向上隐藏新闻条数
downHideFocusNav 下拉是否需要隐藏置顶(1:是,0:否)
这3个参数功能是否还需要? 如果需要,能否放到chConfig中返回?ok
其他逻辑:
列表上划每次都返回置顶top数据,为了兼容客户端展示用,改版后建议客户端修改 待定??
财经频道指数模块特殊处理 ok
第三方对接
统一接口,参数格式化
客户端信息流列表需要对接推荐部门的接口,目前的现状是 推荐的头条接口,频道接口,其他小列表接口和打底接口给出的数据格式均不完全一致,因此为了保证对接的统一性,多次和推荐沟通,将全部接口统一成同一种格式,方便对接和维护。
在过渡期swool端临时进行了数据格式的中转,目的是让web端的逻辑保持统一。
后期推荐将只返回id,其他详细信息将通过后续的业务接口(ucms)进行获取。
配合测试部门梳理历史业务
有些历史业务开发同学也不是特别清楚了,需要测试部门的资深同学进行讲解和梳理,服务端进行及时的整理和细化,将废弃的逻辑大胆删除,简化。
大家好才是真的好
对于很多历史遗留问题,有的不光是一个部门进行优化就能起作用的,需要多个部门配合才能完成,因此需要大家开会讨论,将重构的利弊说清楚,只有大家充分理解重构的优势,才会一起配合,向着最终的方向共同前进。此时需要一定的沟通技巧,将各个业务线的负责人都统一战线,才能取得最终的胜利。
重构的验证
服务端API的重构,相对于客户端调用方来说,是透明的,因此,客户端只需要配合服务端做一些工作即可。
而真正的挑战在于重构后结果的正确性。因为之前的业务逻辑相对复杂,因此最终生成的结果无法简单的进行验证,所以在开发过程中使用了各种方法尽量将最终的数据保持正确。
数据正确性验证
对比小模块生成的json结果
小模块生成的json数据短小,且具有可对比性,修改起来排查也时分方便,借助于网上web端的对比工具也十分方便
JSON对比工具
https://www.sojson.com/jsondiff.html
P.S. BeyondCompare高版本的工具中也内置了json比较的功能,但相对不是很智能且BeyondCompare工具本身对正版的验证较严格,最终放弃。
开发json比对工具
上面提到的web对比工具,在对比长json文档的时候会导致浏览器卡死,无法进行。因此整个信息流最终的输出结果无法直接对比,考虑到这一点,想到手工开发一个比对工具
此工具的实现思路是,将一段json数据的每一个key按字母表顺序排序后,重新进行输出,每一个层级均按此规则实现,于是便有了下面这个递归算法:
/**
* json数组按key名排序
* @param $jsonArray
* @return array
*/
function ksortArray($jsonArray) {
if (!is_array($jsonArray)) {
return $jsonArray;
}
foreach ($jsonArray as $key=> $value) {
if (!is_array($value)) {
continue;
}
ksort($value);
$jsonArray[$key]=ksortArray($value);
}
return $jsonArray;
}
原始日志记录
由于列表中的数据多数来自推荐,导致每一次刷新每个用户的内容是不同的,因此很难再次复现同一条数据。因此为了保证回溯问题,我在接口层面添加了一系列白名单用户,这些用户的请求原始接口的信息会记录详细的返回结果日志,方便后续的比对和验证
由于访问量大,无法全量记录日志排查,仅能针对部分用户做此功能
手机模拟
在抓包工具中进行请求的中转,在实际手机中查看新接口返回的数据显示是否有缺失,测试人员也配合进行多次验证
新旧项目同步开发
人员沟通,代码同步
在重构项目和原有imcp项目并行开发的阶段,由于版本迭代,新加的功能需要在两个项目中同步开发,负责开发的同学也不太一样,实现的方式也略有不同,因此需要及时沟通,同步两个项目的修改
代码更新通知机制
由于部分小需求及bug修改不在版本需求列表中,可能会随时修改上线,因此新项目中为保证不丢失这一部分的更改,在原有项目的上线更新列表中进行监控和通知,有文件变动及时通知到重构项目的开发,保持两端一致
上线方案
上线及回退方案
重构列表上线方案
上线时间安排:
上线时间 | 上线的列表频道 | 量级 |
---|---|---|
2019-11-12 | 推荐列表,关注列表 | 全量 |
2019-11-13 | 头条列表 | 1% |
上线方式:
列表名称 | 上线方式 | 上线客户端版本 | 回退方案 | 回退生效时间 |
---|---|---|---|---|
推荐,关注 | 在cmpp后台频道管理中操作频道api配置,升version版本号 | v6.7.21 | cmpp后台操作回滚 | 用户下次启动app重新获取频道配置后 |
头条 | 在现有头条接口(nlist)中 进行路由跳转,将原有请求转发到重构接口上 | v6.7.21 | 去掉转发逻辑 | 立即生效,再次刷新头条列表即可 |
逐步灰度放量
列表逐步从1%,5%,10%到更多进行切换,方式为在原接口中做302跳转
#region 新头条列表切换
$whiteList=[
'白名单用户id'
];
$hideList=[
'黑名单用户id'
];
if ((version_compare($version, '6.7.21') >= 0 && $idstr == 'SYLB10,SYDT10')) {
if (in_array($uid, $whiteList)||(crc32($uid) % 100 < 5&& !in_array($uid, $hideList))) {
header("Location: https://nine.ifeng.com/headline?" . http_build_query($_REQUEST));
exit;
}
}
#endregion 新头条列表切换
因为原接口中有部分post参数,之前考虑使用307跳转,但是安卓客户端不支持,因此只能使用302跳转,将post参数自动变为get参数提交到新接口中*
列表放量历史
时间 | 客户端类型 | 客户端名称 | 上线频道说明 | 备注 |
---|---|---|---|---|
2019-09-02 17:30 | ios | 专业版,探索版 | “fun来了” 和 “暖新闻” 2个频道 | 推荐位类型频道 |
2019-09-25 17:40 | ios | 专业版 | 推荐频道 | 推荐频道 |
2019-10-11 15:56 | ios | 探索版 | 推荐频道 | 推荐频道 |
2019-10-11 17:14 | android | 快头条 | FUN来了 | 推荐位类型频道 |
2019-10-16 16:38:25 | ios | 探索版,专业版 | 关注频道 | 推荐频道类型+特殊处理逻辑 |
2019-10-16 17:16:28 | ios | 探索版,专业版 | 视频频道 | 推荐频道类型 |
2019-10-17 17:51:39 | ios | 专业版 | 头条 | 头条 |
2019-10-28 16:39:26 | ios | 探索版 | 头条 | 头条 |
2019-11-12 16:42:29 | ios | 主线版 | 推荐频道,关注频道 | 推荐频道类型+特殊处理逻辑 |
2019-11-12 16:56:34 | android | 主线版 | 推荐频道,关注频道 | 推荐频道类型+特殊处理逻辑 |
2019-11-14 15:46:37 | android/ios | 主线版 | 头条 | 上线2%,v6721+,增加白名单 |
2019-11-19 10:14:54 | android/ios | 主线版 | 头条 | 上线5% |
2019-11-22 15:29:12 | android/ios | 主线版 | 头条 | 上线10% |
2019-11-25 15:00 | android/ios | 主线版 | 新正文重新上线 | 全量 |
2019-11-26 14:21:45 | android/ios | 主线版 | 头条 | 上线5% |
2019-11-27 09:43:49 | android/ios | 主线版 | 头条 | 上线15% |
2019-11-28 10:12:27 | android/ios | 主线版 | 头条 | 上线25% |
2019-12-02 10:29:13 | android/ios | 主线版 | 头条 | 上线50% |
2019-12-04 16:47:26 | android | 主线版 | 头条 | 安卓切换10%,ios 50% |
2019-12-06 09:17:18 | ios | 主线版 | 头条 | ios 80% |
2019-12-11 11:34:18 | android/ios | 主线版 | 头条 | 安卓切换40%,ios 100% |
2019-12-12 16:48:46 | android/ios | 主线版 | 头条 | android 40%,ios降到了 30% 版本支持6721-6741版本 |
2019-12-16 14:06:24 | android/ios | 主线版 | 头条 | 全部60% 6721-6750 |
2019-12-18 15:05:35 | android/ios | 主线版 | 头条 | 全部80% |
2019-12-20 10:26:01 | android/ios | 主线版 | 头条 | 90% |
2019-12-23 10:52:53 | android/ios | 主线版 | 头条 | 100% 6721-6750 |
上线后问题处理
2019-11-14 主线版第一次上线头条列表,上线1%,逐步上线后发生过一些问题
redis依赖导致的问题
由于最初使用redis对swool端的降级熔断进行计数等操作,在调用真正业务的api前,会依赖redis的结果进行打底验证,一旦redis发生异常则swool无法对外提供服务。
当时排查到,熔断的逻辑可能有bug 导致进入了死循环,最终redis拒绝连接。
回退新老接口对应
新列表上线前,旧的头条列表中焦点图更多跳转的热点列表,全部使用了重构接口,因此一旦重构项目异常,则无法切换回备用方案。
解决办法是在旧项目中,临时开发了一个数据格式相同的热点列表接口,一旦新项目异常,马上进行切换。
499过多问题排查
逐步放量后,统计监控平台发现日志中499的情况相对较多(安卓端的情况大量多于ios端),于是进行了相关排查。
出现499的原因是由于“客户端”主动断开连接导致,但是实际情况是,服务端API面对的“客户端”不仅仅指真正的手机客户端,期间还好经历负载均衡服务器,代理服务器等的处理,因此需要协调运维的同事一起进行排查。
最终运维给出的答复是:真正的客户端主动断开连接的可能性较大。
于是,我们调研了客户端的相关行为,推测出如下结论:
499的问题级可能是安卓客户端双击返回和crach相关。
在安卓客户端的实现逻辑,在任何频道按一下back键执行下拉刷新,连续按两次back键,直接退出客户端。但是连续两次back的第一次执行下拉刷新,导致这次请求没有结束就关闭客户端了。
另一个原因可能是crach:目前安卓客户端加载文章后存在内存存在不清除的情况,打开多篇文章,大图文章,复杂广告的文章就会引起操作系统级别的内存回收(一个启动的app安卓系统要求在200M以内运行,而我们的客户端启动后大概占140-160M),从而引发crach,这种情况多出现在正文回到列表的时候。
有同事找了几个有499的用户验证了一下,绝大多数499为该用户的最后一条请求。并且 安卓头条499中 86.7%的为down操作,12.3%的为default
最后调查发现,开机配置中,安卓有配置关于back键返回的动作,是导致客户端主动断开的根本原因。
优化与完善
重构头条上线后,基本功能正常,后续还需要进行性能的优化和完善
目前的性能指标
重构头条列表(nine)新版本切量100%后(6721-6750版本)
私有云 QPS:最高270
金山云 QPS:最高170
总共:450 左右
接口平均响应时间 400ms左右
第三方接口优化
列表依赖的主要第三方接口为推荐接口,推荐接口平均响应时间在200ms左右,因此急需优化该接口的响应时长
需要优化的方面:
1.删除服务端不需要的字段
2.简化参数结构,移除字符串json格式的数据
3.协同内容部门接口,改变推荐接口的数据结构,未来考虑只返回id,具体内容详情从内容接口获取
公有云优化
腾讯云的响应延迟还比较大,后续还应继续优化跟进