脚本宝典收集整理的这篇文章主要介绍了大辉谈-备战双十一之动静分离实战,脚本宝典觉得挺不错的,现在分享给大家,也给大家做个参考。
今天准备给大家讲讲我在工作中遇到的困难以及经过各种实践到认知再实践最终实现目标的过程之一: 我是如何思考动静分离架构并最终实现的.
先来说说需求, 我之前所在的团队的商业方向是做电商平台saas,类似于有赞和微盟, 电商saas顾名思义, 它是电商+saas,意思就是普通的电商那一套不够,还得加上saas:)
商品详情的动静分离已经上线了, 截图如下: @H_512_5@
现在不是要双十一了么? 他们都想搞私域的双十一直播,平时的那点流量一下子蹿了十几倍甚至几十倍, 怎么应对? 啊! 动静分离! 团队的研发小伙伴们和问过的朋友以及CEO特意请的顾问都这么说. ok! 那就搞动静分离. 但是具体咋搞? 没人给我好的方案, 因为具体情况具体分析, 动静分离每家每个程序员每个架构师都有自己的想法, 在电商saas的场景下,商品详情, 店铺装修, 商品列表 都是高频请求, 都要实现动静分离, 而且复杂的一点是, 作为saas平台,每个商家的页面和分佣和细节需求都会不一样,这给静态化和动态化增加了困难.
经过一周的头秃思考和实践认知再实践再认知的迭代过程, 我还是最终把动静分离方案实现了出来, 下面我就详细说说我的方法.
首先说下为啥要实现动静分离, 咱们都清楚啊, 首先关系型数据库是有连接数限制的, 如果只是读, 增加只读实例就可以以低成本的方式增加连接数, 但是如果涉及写, 就需要对数据库进行升级. 简单来说, 对于用户的请求, 每次都从数据库获取数据如果连接数够用并且没有额外的SQL执行开销其实并没有什么问题, 问题就在于大量数据的io响应依然会阻碍并发数的提升, 并且会导致系统中的其它业务受影响. 所以解决方案就是nosql, 对常见的用户请求,并不会到达数据库这一级.
先拿商品详情页面来举例子吧, 商品详情页面是商品的展示页面, 在视频号直播时访问的频率最高, 肯定首先要实现商品详情的动静分离, 咱们来先区分下哪些归为静态, 哪些归为动态.
静态内容:
与当前访问者无关的内容为静态内容, 如:
- 商品基本信息
- 优惠券列表信息
- 评价信息
- 商品所属的商家信息
- 商家最新的商品列表
动态内容:
与当前访问者有关的内容为动态内容, 如:
- 已领取的优惠券
- 针对访问者单独显示的优惠券
- 访问者能够拿到的商品的自购返奖金等
出于篇幅问题, 本文只说静态部分, 也就是与访问者无关的页面信息, 咱们来个小目标, 假定有100万用户通过抖音或视频号同时抢购某个限量商品A, 商品A假定库存只有5万件, 可以理解为100万个用户不停的在刷相同的页面, 可以理解为理想情况下要达到100万qps.
如果采用增加数据库读节点的方案,咱们来分析下情况:
基于以上情况, 因为可以2秒内返回, 所以咱们可以假定下每秒只需要达到50万qps就可以了,再看每次请求需要200-300毫秒, 因为在fastapi线程模式下或者flask或者django来说, 每个请求一个线程, 可以理解为每个线程每秒能执行3个请求. 也就是说需要50/3=16.6万个线程. 按照python线程的实际情况, 一般线程数是核数的2-4倍, 假定就是纯io情况, 这里咱们取2倍, 在GIL的情况下, 2倍和4倍其实没什么变化, 实际我测试下来2倍反而更好一些, 每个进程就是cpu_count() 2个线程, 进程数一般也是cpu数的2倍, 按照阿里云的ecs最高配置256核1024G内存的配置, 2562(2562)=262144, 可以理解为需要2台顶级配置的ecs服务器就能够支撑商品详情的请求, 但是考虑到客户端的并发请求情况,咱们豪爽的来4台, 每台阿里云的顶级配置的ecs每小时的费用是56.32元, 4台就是228元, 假定活动前后执行4个小时,可以理解为1000元的成本.
按照每个线程一个连接的映射理论, 就需要17万的数据库连接池连接数. 阿里云按量付费的postgresql数据库最高配置是64核512G, 最大支持51200的连接数,如果要达到17万的连接数, 也就需要至少1台主实例,3台读实例才能够覆盖. 每小时的费用在57*4=228元. 假定活动前后要经历4个小时, 那么总成本就是1000元, 这个还好, 另外存储的成本可以忽略不计.
实际情况是如果前端有并发请求或者还有其他业务也在正常请求, 线程数和数据库连接数上面的计算方法其实根本就不够, 但是上面的计算方式是一个基础数, 在这个基础上, 根据线上业务情况肯定要增加数据库只读实例和ecs服务器数量.
嗯, 100万用户才2000的成本? 错啦!! 阿里云的api网关也要钱, 负载均衡也按小时和流量算钱, 我看了下阿里云, 如果按照每个用户1M的数据返回量来算, 100万用户就是996G的流量, 也就是说这些用户每个都请求一次的成本就是(0.049+0.8)*996=845.6元. 但是这仍然只是一个基础的算法, 前端并发, 大表的请求写入问题, 传输的流量费用, 负载均衡, api网关的费用这些都仍未计算在内呢, 而且每次活动都要预先通知研发团队, 这个对于标准的电商网站都好说, 但是对于做saas平台的就是个噩梦了, 因为根本无法跟商家解释清楚为啥他想卖自己的货需要向平台报备.
所以咱们换个方案, 尝试下使用redis+cdn来抗这100万用户的请求. 方案如下:
大家注意到cdn地址里带了一堆的version字段, 我来解释下这些version是怎么来的.
静态化的redis前面咱们都加个前缀 static: 用来区分不同的业务. 商品详情需要两个redis 哈希表来支持.
static:products:[product_id] 商品哈希表
static:merchants:[merchant_id] 商家哈希表
然后通过领域事件订阅咱们来更新这些版本号
def create_product(rc: Redis,
product_id: int,
supplier_id: int):
rk = f"static:products:{product_id}"
version = int(time.time())
rc.hmset(rk, {
"supplier_id": supplier_id,
"product_version": version,
"comments_version": version
})
@H_256_126@
def refresh_product_version(rc: Redis, product_id: int):
product_rk = f"static:products:{product_id}"
merchant_base_rk = "static:merchants:%s"
version = int(time.time())
redis_eval("refresh_product_version.lua", product_rk, merchant_base_rk, version)
lua脚本
local product_key = KEYS[1]
local merchant_base_key = KEYS[2]
local version = KEYS[3]
local supplier_id = redis.call('hget', product_key,"supplier_id")
local merchant_key = string.format(merchant_base_key,tostring(supplier_id))
redis.call('hset', product_key,"product_version",version)
redis.call('hset', merchant_key,"merchant_version",version)
def refresh_coupons_version(rc: Redis,
merchant_id: int):
version = int(time.time())
rk = f"static:merchants:{merchant_id}"
rc.hset(rk, "coupons_version", version)
def refresh_comments_version(rc: Redis, product_id: int):
rk = f"static:products:{product_id}"
version = int(time.time())
rc.hset(rk, "comments_version", version)
def refresh_merchant_version(rc: Redis,
merchant_id: int):
version = int(time.time())
rk = f"static:merchants:{merchant_id}"
rc.hset(rk, "merchant_version", version)
通过订阅商品详情页面关联的领域事件, 数据的版本号就发生了变更, 这样当客户端请求商品详情的meta信息的时候, 就可以通过lua脚本在redis中读取相关的版本号
def get_product_details_meta(product_id: int) -> Optional[ProductDetailsMeta]:
product_rk = f"static:products:{product_id}"
merchant_base_rk ="static:merchants:s%"
versions = redis_eval("get_product_details_meta.lua", product_rk, merchant_base_rk)
versions_dict = json.loads(versions)
product_version, merchant_version = versions_dict["product_version"], versions_dict["merchant_version"]
meta = ProductDetailsMeta(
supplier_id=product_version.get("supplier_id"),
product_version=product_version.get("product_version"),
comments_version=product_version.get("comments_version"),
coupons_version=merchant_version.get("coupons_version"),
supplier_version=merchant_version.get("merchant_version")
)
return meta
lua脚本
local versions = {}
local product_key = KEYS[1]
local merchant_base_key = KEYS[2]
local supplier_id = redis.call('hget', product_key,"supplier_id")
local merchant_key = string.format(merchant_base_key,tostring(supplier_id))
local function hgetall(hash_key)
local result = redis.call('hgetall', hash_key)
local ret={}
for i=1,#result,2 do
ret[result[i]]=result[i+1]
end
return ret
end
local product_version = hgetall(product_key)
local merchant_version = hgetall(merchant_key)
versions["product_version"] = product_version
versions["merchant_version"] = merchant_version
return cjson.encode(versions)
这里再强调下为什么优惠券的变更是跟随商家的, 因为优惠券的操作肯定是商家操作的, 优惠券的范围可能包含指定商品或集合, 也可能排除指定商品, 但是优惠券肯定是商家创建和修改的, 所以跟踪关系就要建立在商家哈希表上, 虽然商品详情获取优惠券的cdn地址是http://cdn.domain.com/product/[product_id]/coupons_[coupons_version].json, 携带了product_id, 但是咱们关注的其实是coupons_version信息, 只要coupons_version发生了变化,cdn地址肯定是要回源的,通过这种方式保障了实时的静态更新.
现在咱们来算下总成本, 其中cdn我设置的是按天过期, 就是1天就过期, 这样当秒杀请求过去后, cdn也不用承担存储成本.
所以算下来使用redis+cdn的基础成本就是450.6+5.6+144=600.2元. 当然大部分用户肯定会刷页面, cdn的下行资源包实际得买个10T的, 由于通过数据库获取数据的计算也是只计算了1次用户请求的成本,所以粗估成本的时候cdn也是按照只请求一次的成本进行累加. 所以其实成本计算只是个粗估, 并不靠谱, 只是用于判断成本和技术方案, 以及当商家确认要搞个这么大的活动的时候, 确认需要准备多少资源才能支撑.
这是我从晚上6点肝到凌晨2点写完的文章, 写的挺糙的, 因为大辉很久没写这种文章了, 大辉特别能喷, 但是写作自打高中后就没啥自信了, 所以后续的我针对这篇文章肯定还要继续优化. 大家如果有什么问题, 可以私信我或留言.
以上是脚本宝典为你收集整理的大辉谈-备战双十一之动静分离实战全部内容,希望文章能够帮你解决大辉谈-备战双十一之动静分离实战所遇到的问题。
本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
如您有任何意见或建议可联系处理。小编QQ:384754419,请注明来意。