秒杀涉及高读和高写的支持,核心优化理念是高读就尽量“少读"或“读少",高写就数据拆分。
下面从动静分离、热点优化以及服务端性能优化 3 个方面解决性能问题。
动静分离
秒杀过程中你是不需要刷新整个页面的,只有时间在不停跳动。这是因为一般都会对大流量的秒杀系统做系统的静态化改造,即数据意义上的动静分离。动静分离分为三部分:数据拆分、静态缓存、数据整合
数据拆分
将动态页面改造成适合缓存的静态页面。
用户身份信息包括登录状态以及登录画像等,相关要素可以单独拆分出来,通过动态请求进行获取。与之相关的广平推荐,如用户偏好、地域偏好等,同样可以通过异步方式进行加载。
静态缓存
直接缓存整个 HTTP 连接,而不是仅仅缓存静态数据。Web 代理服务器根据请求 URL,可以直接取出对应的响应体然后直接返回,响应过程无需重组 HTTP 协议,也无需解析 HTTP 请求头。
而作为缓存键,URL唯一化是必不可少的,只是对于商品系统,URL 天然是可以基于商品 ID 来进行唯一标识。
缓存三种方式:浏览器、服务端、CDN
浏览器
是第一选择,但用户的浏览器是不可控的,如果用户不主动刷新,系统很难主动地把消息推送给用户,如此可能会导致用户端在很长一段时间内看到的信息都是错误的。对于秒杀系统,保证缓存可以在秒级时间内失效是不可或缺的。
服务端
主要进行动态逻辑计算及加载,本身并不擅长处理大量连接,每个连接消耗内存较多,同时 Servlet 容器解析 HTTP 较慢,容易侵占逻辑计算资源;另外,静态数据下沉至此也会拉长请求路径。
CDN
因此通常将静态数据缓存在 CDN,其本身更擅长处理大并发的静态文件请求,既可以做到主动失效,又离用户尽可能近,同时规避 Java 语言层面的弱点。
CDN缓存有以下几个问题需要解决:
失效问题。任何一个缓存都应该是有时效的,尤其对于一个秒杀场景。所以,系统需要保证全国各地的 CDN 在秒级时间内失效掉缓存信息,这实际对 CDN 的失效系统要求是很高的
命中率问题。高命中是缓存系统最为核心的性能要求,不然缓存就失去了意义。如果将数据放到全国各地的 CDN ,势必会导致请求命中同一个缓存的可能性降低,那么命中率就成为一个问题。
因此将数据放到全国所有的 CDN 节点是不太现实的,失效问题、命中率问题都会面临比较大的挑战。
节点的选取通常需要满足以下几个条件:
- 临近访问量集中的地区
- 距离主站较远的地区
- 节点与主站间网络质量良好的地区
基于以上因素,选择 CDN 的二级缓存比较合适,因为二级缓存数量偏少,容量也更大,访问量相对集中,这样就可以较好解决缓存的失效问题以及命中率问题,是当前比较理想的一种 CDN 化方案。
数据整合
分离出动静态数据之后,需要对数据页整合。
通常有两种方案:ESI(Edge Side Includes)方案和 CSI(Client Side Include)方案。
- ESI 方案:Web 代理服务器上请求动态数据,并将动态数据插入到静态页面中,用户看到页面时已经是一个完整的页面。这种方式对服务端性能要求高,但用户体验较好。
- CSI 方案:Web 代理服务器上只返回静态页面,前端单独发起一个异步 JS 请求动态数据。这种方式对服务端性能友好,但用户体验稍差。
热点优化
热点操作
零点刷新、零点下单、零点添加购物车等都属于热点操作。热点操作是用户的行为,不好改变,但可以做一些限制保护,比如用户频繁刷新页面时进行提示阻断。
热点数据
热点识别
热点数据分为静态热点和动态热点。
- 静态热点:能够提前预测的热点数据。大促前夕,可以根据大促的行业特点、活动商家等纬度信息分析出热点商品,或者通过卖家报名的方式提前筛选;另外,还可以通过技术手段提前预测,例如对买家每天访问的商品进行大数据计算,然后统计出 TOP N 的商品,即可视为热点商品
- 动态热点:无法提前预测的热点数据。冷热数据往往是随实际业务场景发生交替变化的,可能一件商品在短时间内被大量购买。由于此类商品日常访问较少,即使在缓存系统中一段时间后也会被逐出或过期掉,甚至在 DB 中也是冷数据。瞬时流量的涌入,往往导致缓存被击穿,请求直接到达 DB,引发 DB 压力过大。
因此秒杀系统需要实现热点数据的动态发现能力,热点数据采集最好采用异步方式,一方面不会影响业务的核心交易链路,一方面可以保证采集方式的通用性。热点发现最好做到秒级实时,这样动态发现才有意义,实际上也是对核心节点的数据采集和分析能力提出了较高的要求。
一个常见的实现思路是:
- 异步采集交易链路各个环节的热点 Key 信息,如 Nginx 采集访问 URL 或 Agent 采集热点日志(一些中间件本身已具备热点发现能力),提前识别潜在的热点数据。
- 聚合分析热点数据,达到一定规则的热点数据,通过订阅分发推送到链路系统,各系统根据自身需求决定如何处理热点数据,或限流或缓存,从而实现热点保护。
热点隔离
热点数据识别出来之后,需要将热点数据隔离出来,不要让 1% 影响到另外的 99%。比如按照用户来区分,为不同的用户分配不同的 Cookie,入口层路由到不同的服务接口中。域名保持一致,但后端调用不同的服务接口。在数据层给数据打标进行区分。
可以基于以下几个层次实现热点隔离:
- 业务隔离。秒杀作为一种营销活动,卖家需要单独报名,从技术上来说,系统可以提前对已知热点做缓存预热。
- 系统隔离。系统隔离是运行时隔离,通过分组部署和另外 99% 进行分离,另外秒杀也可以申请单独的域名,入口层就让请求落到不同的集群中。
- 数据隔离。秒杀数据作为热点数据,可以启用单独的缓存集群或者 DB 服务组,从而更好的实现横向或纵向能力扩展。
热点优化
- 缓存:热点缓存是最为有效的办法。如果热点数据做了动静分离,那么可以长期缓存静态数据
- 限流:流量限制更多是一种保护机制。需要注意的是,各服务要时刻关注请求是否触发限流并及时进行review
系统优化
减少序列化
减少 Java 中的序列化操作可以很好的提升系统性能。序列化大部分是在 RPC 阶段发生,因此应该尽量减少 RPC 调用。
直接输出流数据
只要涉及字符串的 I/O 操作,无论是磁盘 I/O 还是网络 I/O,都比较耗费 CPU 资源,因为字符需要转换成字节,而这个转换又必须查表编码。所以对于常用数据,比如静态字符串,推荐提前编码成字节并缓存,具体到代码层面就是通过 OutputStream() 类函数从而减少数据的编码转换。另外,热点方法 toString() 不要直接调用 ReflectionToString 实现,推荐直接硬编码,并且只打印 DO 的基础要素和核心要素。
裁剪日志异常堆栈
无论是外部系统异常还是应用本身异常,都会有堆栈打出,超大流量下,频繁的输出完整堆栈,只会加剧系统当前负载。可以通过日志配置文件控制异常堆栈输出的深度。
去组件框架
极致优化要求下,可以去掉一些组件框架,比如去掉传统的 MVC 框架,直接使用 Servlet 处理请求。这样可以绕过一大堆复杂且用处不大的处理逻辑,节省毫秒级的时间。