饿了么黑客马拉松参赛杂谈

13 篇文章 0 订阅
11 篇文章 0 订阅

饿了么黑客马拉松参赛杂谈

在上个月参加了饿了么黑客马拉松,虽然没有拿到奖有点遗憾,但是感觉学到了很多技术,很值得,遂记录下来。

赛制

使用 Python, Java, Go 三种语言(任选其一)实现一个“限时抢购”功能

评分标准

评分使用功能测试、性能测试的结果做为指标

功能测试

通过所有单元的功能测试才会进入性能测试评分,否则性能测试分数将为0

根据 API 规范完成 7 个接口(登录,查询库存,创建购物车,添加食物,下单,查询已下订单,查询系统所有订单)

性能测试

以“成功下单数/秒”的峰值及准确率作为评分标准

  1. 给定 100 个食物,每个库存 1000

  2. 脚本模拟 1000 个并发用户同时进行抢单。(登录 -> 查看食物列表 -> 随机选择 1-3 个食物购买 -> 下单)

  3. 排行榜将按照“成功下单数/秒”的峰值进行排名

  4. 如出现多名选手“成功下单数/秒”的相差 ≤10 的情况,将根据准确率决定最终的排名
    测试环境(所有服务器基于腾讯云)

  5. 2CPU-4GB 服务器 * 3,运行应用。(请求会随机分布到 3 台服务器上)

  6. 2CPU-4GB 服务器 * 1,提供 redis

  7. 25GB 1000qps 腾讯云数据库,提供 mysql

  8. 12CPU-12GB 服务器 * 1,运行性能测试脚本

感想

其实题目要实现的功能并不复杂,就是限时抢购功能。他可以选择Go,Java和Python三种,因为本身比较喜欢Python,写起来也好写,就在明知性能不怎么行的情况下先用Python尝试了,其实这就输在起跑线上了,我觉得三个语言一起排名是不怎么公平的,最后榜上都没有Python的。考虑到性能,就不用什么Flask,Tornado之类的框架了(后来试过,性能更差),直接使用WSGI+gunicorn+gevent,而在储存数据方面,考虑到Mysql并不适用于高并发情况下的场景,就决定不使用Mysql了,直接使用redis储存数据。

如果要把性能发挥到极致,其实很多东西都自己写才是最好的,因为框架中会有很多冗余的地方,用不着。为了性能,也牺牲了代码的可读性。在WSGI server方面,我查找过很多解决方案,一开始使用的是gunicorn+gevent,后来测试gunicorn+meinheld性能会更好,最后发现是纯C写的bjoern能够达到最佳的性能。总的来说就是gevent < meinheld < bjoern。

在优化方面,主要是考虑到异步,缓存,减少IO,优化数据结构,减少阻塞等。分析出瓶颈所在,针对这些方面去想办法就好。

设计数据结构的时候,主要考虑如何方便查询,并且查询次数尽量减少,我是这样设计购物篮子的:每个购物篮子是一个列表,结构是cart_id对应一个[user_id,food_id,food_id,food_id],这样的话,用户创建的时候就是cart_id对应[user_id],这样把用户id也放进去就不用额外保存这个购物篮子跟用户关系了,添加食物直接RPUSH即可。储存订单就是直接生成订单的一个字符串,使用订单号来对应这个字符串,这样就省去查询时生成JSON的时间了。

在程序启动时,就可以把数据从Mysql读取到Redis中,并且保存部分数据在缓存中(用户信息,食物信息(不包括库存))。

尝试过把部分redis访问交给celery的worker处理,想让很多操作变成异步,这样就可以让部分请求不等待操作完成直接返回了,发现效果不佳,可能是因为使用了redis作为broker而不是rabbitmq。

尝试使用multiprocessing启动,然后用共享变量的方法来缓存订单信息,每台机器在完成下单后,保存这个订单,这样理论上命中率有1/3,会有一定提升的,但测试结果显示性能也没有什么提升,可能是共享变量自动加锁导致的。

在查询库存那踩了个大坑,我认为的是每查询一次都是返回当时所查询到的库存,结果测试中并没有考虑到这个,官方后来的解释是在高并发情况下无法实时更新这个数据,所以说这个是不重要的,我觉得这就是一个坑人的接口。如果考虑到这个的话,其实可以将数据保存在内存中,隔一段时间再刷新一次,然后请求的时候直接从内存获取数据返回,后来测试过,这样写的话性能就能够提升20%。

要尽量减少对数据库的访问,所以在涉及到多处数据库访问时,使用了pipeline和lua脚本的方法,使得多处的数据库访问变成一次。pipeline可以把多条合并成一条发送执行,减少了IO。lua脚本的好处是保证了操作的原子性还减少IO,坏处可能是加大了redis服务器的负担,这样把很多逻辑都写进了lua里了,然后再lua脚本中判断请求是否合法等,返回一个值,根据这个值来获取处理情况。

另外看到别人说的方法,应该是挺有效的,就是使用Pub/Sub的方法来进行三台机器的同步,这样虽然实现起来比较麻烦,优点在于大部分的数据都可以使用本地缓存,可以大大提升速度。例如:

由于机器数量3台已知,库存总量1000已知。所以从理论上说,只要单台机器上没有商品被卖到333个以上是不可能出现超卖的。所以完全可以让前期下单判断库存是否足够放在本地,一旦有一个商品销量超过333,就使用publish通知到三台主机上,之后的下单逻辑按正常进行,老老实实走redis。

具体操作是,用全局flag标记本地是否有商品售卖超过333,前期每次来订单,只要合法都成功下单,并在本地记录每个商品销量,一旦有大于333销量的商品出现,则:

publish 通知到三台机器上
本机修改全局标记,同时将缓存的销量pipeline到redis里做扣除

每台机器收到通知都进行步骤2

再后来,实现已经达到了瓶颈,我把能想到可以优化的地方都优化,已经做到了每个操作最多使用一次数据库的访问(有些操作不用),性能还是上不去,所以改成用Go来写,几天速成了Go语言,用据说很快的Gin框架把原本的Python代码翻译了一遍,一跑果然快了很多,在Python中想各种黑科技还不如用Go随便一写快。想要吐槽的是Gin的文档不是很清晰,很多地方都要直接看源码才知道他是怎样的。而Go这个号称有C的运行效率和Python的开发效率的语言也很蛋疼,感觉很多地方不怎么人性化,可能还需要一定时间去熟悉,不过在性能方面确实是不错的,以后有机会可能还会用到的。

总的来说,这次比赛学到了很多知识,特别是在高并发环境下的处理,了解了很多框架以及他们的实现原理等,使用到了许多新的技术,例如Go语言和Lua语言,也向大家学习了很多东西。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值