python爬取自如网房源:贝壳找房小程序从PHP到Golang的跃迁之路
python爬取自如网房源:贝壳找房小程序从PHP到Golang的跃迁之路于是,当前服务层要想优化,比较好的选择是:增加cache。P=(1-P1)*(1-P2)*(1-P3)*……*(1-PN)小程序前台项目的特点是:IO密集型服务小程序目前依赖众多下层服务,一个普通的小区接口依赖的下层API达到11个之多。在阻塞IO的模式下,所有等待延迟串行叠加,非常容易给前端造成比较高的后台等待,影响用户体验。特别是遇到个别服务的部分请求出现透传DB,SQL效率不高时,就更会雪上加霜,499突增,威胁到服务稳定性。这样一来,对于一个PHP的N层调用场景来说,任何一层服务的失败都会直接影响终端获取数据的成功率,当每层及时成功返回数据的概率为P,则它的最终成功率就会变成:
1. 前言
1.1 PHP是最好的语言
PHP确实有非常强大的优势。对于中小型Web服务,业务具有高度不确定性,产品迭代速度是第一目标,非常适合使用PHP作为创业启动语言。
1.2 使用PHP遇到的问题
小程序前台项目的特点是:IO密集型服务
小程序目前依赖众多下层服务,一个普通的小区接口依赖的下层API达到11个之多。在阻塞IO的模式下,所有等待延迟串行叠加,非常容易给前端造成比较高的后台等待,影响用户体验。特别是遇到个别服务的部分请求出现透传DB,SQL效率不高时,就更会雪上加霜,499突增,威胁到服务稳定性。
这样一来,对于一个PHP的N层调用场景来说,任何一层服务的失败都会直接影响终端获取数据的成功率,当每层及时成功返回数据的概率为P,则它的最终成功率就会变成:
P=(1-P1)*(1-P2)*(1-P3)*……*(1-PN)
于是,当前服务层要想优化,比较好的选择是:增加cache。
好处是减少向后请求,透传db的情况也会顺应得到一定的改善,耗时也会因为跳过了逻辑计算而得到收敛。
其后果是牺牲了实时性,换取性能的提高。然而,这可能导致房屋标价,出现短时显示不稳定;租价相差几百,卖价相差几千,都会让客户抓狂。性能提高的程度要看缓存命中率;过期时间长短需要平衡,过久则时效性差,过短则命中率不高。核心城市的购房者和卖房者相对密度大一些,而二三线城市就没那么好了。
多层cache情况下,每层设置cache过期时间为T,当有变更发生需要最终生效时,其最糟糕生效时间是:
T=T1 T2 T3 ……Tn
综上,伴随着调用模块数量的增加,延迟超标的接口会越来越多;增加cache之后,信息展示的不确定性会增多。左右为难,然如何破局?
1.3 选择Golang的原因
了解过Golang的同学,以下部分特性可能比较吸引你:
- Goroutine协程
- 多路复用
- Channel通道
小程序团队主要考虑以下几个点:
- 协程:将多个模块的拉取和格式化作为独立单元,进行单元级别并行
- 异步IO:节省CPU,提高服务并发能力
- Channel通道:可以将一些轻量的异步动作用协程 channel实现,简化架构
- 强大的标准库:可以实现和PHP等价的逻辑
- 稳定性:常驻运行也依然稳定
- 丰富的工具:解决单元测试、代码风险检查
- 部署运行环境:贝壳已经支持线上运行和托管Golang服务
最终的收益将会体现为:
- 降低当前用户体验延迟
- 遏制接口延迟持续恶化
- 提高服务的稳定性
有的人说,为什么不使用swoole呢?从BAT的应用情况来看,swoole并未受到追捧;且swoole也可能像hhvm一样,只是过渡期选择。
有的人说,guzzle不香吗?跟协程能做的事情来讲,我们更看重模块级别的并行,而不仅仅是IO。
1.4 Golang为什么快
按照Golang布道师Dave Cheney在2014年Gocon大会上的分享来看,其中5点值得注意:
- 变量的处理和存储一个int32的变量只占用4字节,但是在python中需要24字节,在Java中需要16-24字节。内存占用少,使得CPU的cache效率更高。数组变量紧凑的内存结构,避免了无谓的指针跳转。
- 函数内联虽然增加了包大小,但是减少了函数调用的开销,特别是很多短小的函数。
- 垃圾回收通过逃逸分析方法,使得更多的变量申请空间可以在栈上得到分配,减轻了堆上资源垃圾回收的压力。一般程序员不需要关心是在栈上还是堆上。
- 协程在利用线程对进程来优化的思路基础上,进行了延伸。协程非常轻量,再加上协程切换一般发生在阻塞、系统调用、垃圾回收等时机,从而减少了等待。每个Go进程只需要少量系统线程,Go的runtime会将协程放到空闲操作系统线程上运行。
- Goroutine的栈管理通过去掉guard page,在函数调用时增加栈空间检查的机制,并在必要时自动分配更多空间的方式,使得初始的空间可以很小,goroutine可以很轻量。而函数调用带来的频繁扩缩容,利用连续栈分配更大空间而得到解决。
2. 实践
2.1 目标
当时域名下114个API,大部分的延迟都不高,在调用量比较大,延迟比较突出的二手接口中,我们锁定了部分接口作为改造对象。
目标
二手业务8个接口
投入人力
3人
时间成本
6周
编码量
约2w行
2.2 实施步骤
2.2.1 业务框架
没有业务框架,直接进行业务重构就是耍流氓。但19年,公司还没有出品Golang业务框架,我们得自己搞定。
先向杨宇(比克)学习了沙场项目,获得了在贝壳服务器安装、运行、部署Golang服务的基本经验。宇哥多次指导,深表感谢。
使用涉及的工具有:
- go mod:搞定代码包管理
- go proxy:解决拉取内外仓库代码的差异问题
- gin:搭建基础Golang web服务的简洁框架
2.2.2 跑通第一个接口
万丈高楼从地起,我们进行了以下关键实践:
- 协程并行封装:避免每个人单独封装,单独调试的麻烦。
- 对接部署环境:封装build逻辑,实现打包编译、环境变量管理。
- 超时管理:进行区分环境的不同超时设置,包括框架超时和http调用超时;将connect超时和read超时区分开。
- 签名验签:解决前后端验签实现,解决http调用验签实现。
- 日志分级:完成warning、fatal、panic、access等日志的分封。
- 监控告警:利用日志收集到fast,通过看板和日志报警管理稳定性,最为重要的是内存监控和协程数监控。
- 单元测试:利用go test实现基础代码的单测,避免偶现bug在未来长时间内不定时跑出来,并产生灵异现象。
- local cache:在全局下发的一些公共数据上,可以达到比较好的加速效果。
- 环境区分:通过不同环境加载配置,实现环境隔离。
- 配置治理:将环境变量、命令行参数、配置文件统一规范为配置文件,保持单一入口和拉起程序的简化。
- 打包机联调:将环境差异干预配置到打包机,避免同一个文件在git仓库管理多个版本。
- Diff平台:通过diff工具进行接口字段对比,避免逻辑丢失和差异。
- 自动部署:通过CI平台的Jenkins来实现代码到测试环境的自动部署,快速提高bug的解决和验测节奏。
- 热编译:通过对开源项目gowatch的二次开发,实现了代码自动编译和附带工具运行,避免写一行编一次。
- 目录管理:主要分离了base、data、model、controller几层,最大限度保持大家写PHP的习惯迁移。
- 服务保活:和OP一起适配systemd服务,在服务宕机时可以秒起进程。
在我们完成第一个接口nearby的逻辑编码的过程中,以上80%的特性也一起附带实现了,整个过程为10个整天(对语言不熟悉的话,需要适当添加时间;剩下20%是陆续边改造边实现的)。另外7个接口的逻辑重构工作,就可以据此为模板展开了。
但此时接口还没有增加redis缓存,新接口延迟比老PHP接口明显高出一截。
2.2.3 优化接口
加缓存可能是接口优化最直接有效的手段了,而且成本一般都不是问题;但请神容易送神难。丢失的实时性很难找回,带来的问题很难定位。数据显示错误时,是哪一层缓存错了,没有谁能说得清。所以我们打算在不加缓存的情况下,去优化延迟。
Golang最大的特性是协程并发,所以我们迫不及待开始用协程来试试效果。
在nearby接口中,我们分析出了2层逻辑可并行的场景:
- getErshouList、getNewfangList、getZufangList实现并行
- SearchResblockHouseSell和GetResblockSell实现并行
这样一来,解决的延迟就轻松超过了100ms。
在后来的xiaoqu接口中,有更加优异的表现,整体实现了4层逻辑并行。
并行代码难写吗?并非如此。
封装一层是降低代码复杂度的有效武器;如果不是,就再封装一层。base包下实现一个GoWait方法,并行调用就会非常优雅:
关于缓存,我们斟酌再三,最后还是在房源列表增加了cache,理由为:
假如,用户在列表看到10条房源信息,当他点击第一条,进入详情页能看到最实时的信息,时效性是以详情页为准,所以体验没有问题;
当用户返回列表点击第二条时,列表未刷新,则列表展示的第二条可能已经过时;如果第二条已经被删掉,点击会出现404;删除的那条,用户不一定会点击;此时我们如果更新列表,则可能发生位置或者或多或少的变化,用户感受到了不稳定;
所以最终,我们在房源列表这里对列表房源信息增加了redis的cache,保证了底层页的时效性和列表页的稳定感。
2.2.4 性能收集
一个事物,如果你不能准确的观察它,那你就很难控制它。
Golang服务容易暴露出线程安全、panic、泄露、空指针引用等问题,最为常见的就是协程泄露。监控协程数,是区分协程泄露还是句柄泄露的关键举措。
其次是资源泄露,如果服务频繁宕掉,极可能是panic未捕获,或者内存过高引起的oom。panic的问题可以通过添加默认recover的方式解决。内存过高被杀会影响服务稳定性,好在内存不是一分钟就涨上去的,我们可以在达到一定限度时就开始介入,避免宕机才后知后觉。
幸运的是,Golang的runtime可以轻松实现协程数和内存(并非实际内存)的获取。利用fast天眼平台的看板视图,很容易就做到服务的监控走势图:
如果是每天周期性波动,可以暂时松一口气。部分边界问题可能后期暴露,从而打破平静,建议增加阈值告警,避免上升为故障才后知后觉。这方面已经有不少先例了,痛过的人自然明白。
2.2.5 配套工具
为了更好的保证golang服务的质量,尽早发现问题,我们还引入了以下工具,大家可以根据情况选用:
go generate
自动生成代码
go fmt
代码格式化
go mod tidy
精简代码包
go mod vendor
拷贝包到vendor目录
go vet
检查代码错误
golint
检查代码风格
goswagger
Swag文档工具
gowatch
热编译工具
配合起来,就能够实现如图的连续编码体验:
大概就是三个节点的循环,体验跟PHP开发不相上下:
好的体验就能促就更多的人参与,目前项目里面5个人可以熟练进行Golang开发,新需求优先选择Golang服务实现。这样就不会因为个别人跑路而导致项目无人维护的了。大家都非常喜欢Golang这种标准、简洁、稳定、强大、易上手的语言。
2.2.6 灰度上线
这将是我们团队年度最重大变更之一,需要严格把控风险。把控风险最好的方式,就是将功能做成可灰度,避免一刀切。
我们在前端启动接口中下发配置,允许前端在命中灰度的情况下将调用域名切换到新接口。而新接口在调用方式上完全等同老接口,包括path、method、参数结构、验签、header规则。所以仅有域名差异,前端比较容易实现。
通过apollo实现离线配置,实时控制灰度程度。
当前端发现新域名不可用时,兜底使用老域名容错。
2.3 最终收益
通过此番改造,产生了以下效果:
- 稳定性提升0.02%左右
- 平均延迟降低15.6%
- TP90延迟降低37.5%
而这个收益是在我们没有利用redis来给业务模块大面积覆盖cache而损失时效性的基础上得到的,同时未来也会因为可以使用并行,从而接口延迟快速上涨的情况得到很大的遏制,从而也减少了因为延迟上涨到不可接受,而被迫进行的重构工作。
服务上线8个月以来,一直稳定运行,且保持了健康快速的迭代,没有故障,这让我们有底气将经验分享给小伙伴们。
2.4 进一步规划
使用公司统一框架,完成组件标准化。
3. 结语
Golang是一门比较新的语言,用好了可以在不明显增加负担的情况下,带来不错的效果。希望在能给业务带来明显收益的前提下,更多的项目能够试用,并从中尝到甜头。上到Web服务,下到框架、组件、中间件,Golang都有很不错的应用,前途看好。