这是我参与「第五届青训营 」笔记创作活动的第10天
1 引言
1.1 为什么要做系统设计
- 对公司而言。 设计系统是一个产品的基础,也是业务快速迭代的发动机。通过设计系统的建设,可以优化业务生产流程,达到企业降本提效的目的。
- 对设计团队而言。 设计系统是设计团队最为核心的能力和资产,是设计团队提升影响力、成为业务核心能力的重要基础。
- 对设计师而言。 设计系统是设计师体系化思考的能力沉淀,也是设计师职业发展不可或缺的一种重要能力。
1.2 如何做系统设计
1、场景分析
2、存储设计
3、服务设计
4、扩展性
1.3 如何分析系统瓶颈和优化
1.3.1 火焰图分析
火焰图(FlameGraph)是 svg 格式的矢量图,是先通过 perf 等工具分析得到结果,并将该结果生成的具有不同层次且支持互动的图片,看起来就像是火焰,这也是它的名字的由来。
需要注意以下几点:
1、 纵向(Y 轴)高低不平,表示的是函数调用栈的深度。每一层都是一个函数。调用栈越深,火焰就越高,顶部就是正在执行的函数,下方都是它的父函数。
2、 横向(X 轴)表示该函数执行消耗的时间,横向上会按照字母顺序排序,而且如果是同样的调用会做合并(注意:如果一个函数在 X 轴占据的宽度越宽,就表示它被抽到的次数多,即执行的时间长,所以这里不是严格意义上的执行消耗的时间),所以一个横向宽度越大的函数调用,一般很可能是程序的瓶颈。
3、 火焰图的颜色是随机分配的,并不是颜色越深就是越瓶颈。因为火焰图表示的是 CPU 的繁忙程度,所以一般都是暖色调。我们需要留意的就是那些比较宽大的火苗。只要有"平顶",就表示该函数可能存在性能问题。
要生成火焰图,就需要一个 Tracer 软件,通过该软件在系统上的运行过程采样,得到结果,并将该结果图形化,才可得到人眼直观可视化的 svg 格式数据矢量图。
1.3.2 链路分析
全链路监控组件就在这样的问题背景下产生了。最出名的是谷歌公开的论文提到的 Google Dapper。
想要在这个上下文中理解分布式系统的行为,就需要监控那些横跨了不同的应用、不同的服务器之间的关联动作。
所以,在复杂的微服务架构系统中,几乎每一个前端请求都会形成一个复杂的分布式服务调用链路。一个请求完整调用链可能如下图所示:
同时我们会关注在请求处理期间各个调用的各项性能指标,比如:吞吐量(TPS)、响应时间及错误记录等。
- 吞吐量,根据拓扑可计算相应组件、平台、物理设备的实时吞吐量。
- 响应时间,包括整体调用的响应时间和各个服务的响应时间等。
- 错误记录,根据服务返回统计单位时间异常次数。
全链路性能监控从整体维度到局部维度展示各项指标,将跨应用的所有调用链性能信息集中展现,可方便度量整体和局部性能,并且方便找到故障产生的源头,生产上可极大缩短故障排除时间。
1.3.3 全链路压测
评估系统容量或者准确的说评估线上系统的容量现阶段最优效也是最准确的方式就是进行线上全链路压测。
第一步,确定“全链路”应该包含链路(或顶级接口),所谓的全链路,它其实是一个相对的概念,在刚开始做全压时,我们主要是把线上的核心链路找出来,找到这些链路的顶级接口,这其实就是发压的主要入口。
第二步,确保压测标识在这些链路中传递以及处理,第二步是最难的也是最复杂的,我们要分析第一步中这些链路中如何有效安全的传递压测标识,压测标识是系统中用来区分压测流量还是线上正常流量的标识,我们要保证压测标识正确的传递和清除,否则可能导致严重的线上事故。
2 电商和秒杀
2.1 概念
2.1.1 SPU
SPU(Standard Product Unit):标准化产品单元。是商品信息聚合的最小单位,是一组可复用、易检索的标准化信息的集合,该集合描述了一个产品的特性。通俗点讲,属性值、特性相同的商品就可以称为一个SPU。
2.1.2 SKU
最小存货单位(SKU),全称为Stock Keeping Unit,即库存进出计量的基本单元,可以是以件,盒,托盘等为单位。SKU这是对于大型连锁超市DC(配送中心)物流管理的一个必要的方法。现在已经被引申为产品统一编号的简称,每种产品均对应有唯一的SKU号。单品:对一种商品而言,当其品牌、型号、配置、等级、花色、包装容量、单位、生产日期、保质期、用途、价格、产地等属性中任一属性与其他商品存在不同时,可称为一个单品。
2.2 设计秒杀系统
2.2.1 动静分离
动静分离的首要目的是将动态页面改造成适合缓存的静态页面。因此第一步就是分离出动态数据,主要从以下 2 个方面进行:
用户。用户身份信息包括登录状态以及登录画像等,相关要素可以单独拆分出来,通过动态请求进行获取;与之相关的广平推荐,如用户偏好、地域偏好等,同样可以通过异步方式进行加载 时间。秒杀时间是由服务端统一管控的,可以通过动态请求进行获取。
分离出动静态数据之后,第二步就是将静态数据进行合理的缓存
静态数据缓存到哪里呢?可以有三种方式:1、浏览器;2、CDN ;3、服务端。
浏览器当然是第一选择,但用户的浏览器是不可控的,主要体现在如果用户不主动刷新,系统很难主动地把消息推送给用户(注意,当讨论静态数据时,潜台词是 “相对不变”,言外之意是 “可能会变”),如此可能会导致用户端在很长一段时间内看到的信息都是错误的。对于秒杀系统,保证缓存可以在秒级时间内失效是不可或缺的。
服务端主要进行动态逻辑计算及加载,本身并不擅长处理大量连接,每个连接消耗内存较多,同时 Servlet 容器解析 HTTP 较慢,容易侵占逻辑计算资源;另外,静态数据下沉至此也会拉长请求路径。
因此通常将静态数据缓存在 CDN,其本身更擅长处理大并发的静态文件请求,既可以做到主动失效,又离用户尽可能近,同时规避 Java 语言层面的弱点。
2.2.2 热点优化
零点刷新、零点下单、零点添加购物车等都属于热点操作。热点操作是用户的行为,不好改变,但可以做一些限制保护,比如用户频繁刷新页面时进行提示阻断。
热点数据分为静态热点和动态热点,具体如下:
静态热点:能够提前预测的热点数据。大促前夕,可以根据大促的行业特点、活动商家等纬度信息分析出热点商品,或者通过卖家报名的方式提前筛选;另外,还可以通过技术手段提前预测,例如对买家每天访问的商品进行大数据计算,然后统计出 TOP N 的商品,即可视为热点商品
动态热点:无法提前预测的热点数据。冷热数据往往是随实际业务场景发生交替变化的,尤其是如今直播卖货模式的兴起——带货商临时做一个广告,就有可能导致一件商品在短时间内被大量购买。由于此类商品日常访问较少,即使在缓存系统中一段时间后也会被逐出或过期掉,甚至在db中也是冷数据。瞬时流量的涌入,往往导致缓存被击穿,请求直接到达DB,引发DB压力过大 因此秒杀系统需要实现热点数据的动态发现能力,一个常见的实现思路是:
异步采集交易链路各个环节的热点 Key 信息,如 Nginx采集访问URL或 Agent 采集热点日志(一些中间件本身已具备热点发现能力),提前识别潜在的热点数据 聚合分析热点数据,达到一定规则的热点数据,通过订阅分发推送到链路系统,各系统根据自身需求决定如何处理热点数据,或限流或缓存,从而实现热点保护
2.2.3 系统优化
对于一个软件系统,提高性能可以有很多种手段,如提升硬件水平、调优JVM 性能,这里主要关注代码层面的性能优化——
减少序列化:减少 Java 中的序列化操作可以很好的提升系统性能。序列化大部分是在 RPC 阶段发生,因此应该尽量减少 RPC 调用,一种可行的方案是将多个关联性较强的应用进行 “合并部署”,从而减少不同应用之间的 RPC 调用(微服务设计规范)
直接输出流数据:只要涉及字符串的I/O操作,无论是磁盘 I/O 还是网络 I/O,都比较耗费 CPU 资源,因为字符需要转换成字节,而这个转换又必须查表编码。所以对于常用数据,比如静态字符串,推荐提前编码成字节并缓存,具体到代码层面就是通过 OutputStream() 类函数从而减少数据的编码转换;另外,热点方法toString()不要直接调用ReflectionToString实现,推荐直接硬编码,并且只打印DO的基础要素和核心要素
裁剪日志异常堆栈:无论是外部系统异常还是应用本身异常,都会有堆栈打出,超大流量下,频繁的输出完整堆栈,只会加剧系统当前负载。可以通过日志配置文件控制异常堆栈输出的深度
去组件框架:极致优化要求下,可以去掉一些组件框架,比如去掉传统的 MVC 框架,直接使用 Servlet 处理请求。这样可以绕过一大堆复杂且用处不大的处理逻辑,节省毫秒级的时间,当然,需要合理评估你对框架的依赖程度