系统设计大纲

系统设计大纲 #

理论 #

分布式系统设计三大定理

  • CAP:一致性(Consistency)、可用性(Availability)、分区容忍性(Partition tolerance)。
    • CAP指出分布式系统应该在保证分区容忍性的前提下,在一致性和可用性间进行抉择。如果需要保证线性一致性,那么断开的节点就不能提供服务,因为其上的数据可能是旧的。
    • 实现强一致性的唯一实现方式同一时刻只允许一个请求更新,常用实现方式为在更新前加锁或者将所有写请求排队。
  • BASE:基本可用(Basically Available)、软状态( Soft-state)、最终一致性(Eventually consistent)
  • PACELC:P则A或C,Else则L或C。
    • 如果出现网络分区(P),分布式系统应该在可用性(A)和一致性(C)间进行权衡。
    • 否则(E),分布式系统应该在延迟(L)和一致性(C)间进行权衡。

系统设计原则

  • 单一职责:也称关注点分离。每个组件负责特定的功能,且只关注自己的功能。组件间松耦合,修改一个组件不会影响到另一个组件。不同服务间可以在不互相影响下重用组件(如使用同一数据库)。
    • 存储过程很明显地违反了单一职责。

系统架构方案 #

宏观、微观架构 #

  • 微观架构:单个微服务的架构。主要涉及授权、功能测试、持续集成、性能指标等。
  • 宏观架构:多个微服务的统一架构。主要涉及身份认证、通信协议(Restful)、通信格式(JSON)、集成测试等。
  • 对比:
    • 编程语言、数据库、文档、UI等既可以使用微观架构也可以使用宏观架构。
    • 每个微服务都有自己的微观架构;宏观架构应尽可能适用于系统中的所有微服务。
    • 宏观架构影响微观架构,微观架构的技术选择应保证能实现宏观架构。
    • 宏观架构的使用次数应该得到控制,过多的统一规则会破坏不同微服务间的独立性。

非阻塞架构 #

alt

非阻塞架构也被称为事件驱动架构(Event-driven)或反应式架构(Reactive)。系统本质是异步的,会对当前发生的事件做出反应。为了实现该 功能,系统必须持续监视事件流。

反应式、事件驱动的应用程序很难用基于线程的框架来实现,因为用线程编写非阻塞代码时必须仔细处理共享的可变状态和锁等,这会非常复杂。所以在这种系统中,一般底层的线程会被进行抽象为接受和响应事件的Event Loop和执行长时间后台任务的Worker。

无共享架构 #

无共享架构意味着消除所有单点故障。每个模块都有自己的内存和磁盘。因此,即使系统中的几个模块发生故障,其他的在线模块也不会受到影响。无共享架构有助于提高可扩展性和性能。微服务通常都采用无共享架构。

六边形架构 #

alt

六边形架构的设计目标是使应用程序的组件独立、松散耦合且易于测试,无论调用时是否使用UI,模拟数据库和模拟中间件,都不需要对代码进行更改。

在六边形架构中,核心为领域逻辑(业务逻辑);最外层为端口,负责所有I/O操作,包括API请求、消息队列、数据库等;中间层为适配器,负责将端口获得的数据转换为业务逻辑需要的数据。外部实体与内部业务逻辑没有任何交互。

六边形架构类似常见的Controller-Service-Dao的分层架构,没有太大区别。但传统分层方法有一个问题。除了标准控制器、服务、数据访问外,开发人员通常会创建太多的层。这会使业务逻辑分散在多个层上,使得测试、重构和插入新实体变得困难。

点对点网络架构 #

alt

P2P网络架构可以在没有中央节点的情况下彼此通信。因为没有中央节点,所以没有单点故障。所有节点都有平等的权利。每个节点都可以上传或下载数据。

P2P网络架构中其它节点的发现方式

  • 非结构化网络:随机连接。响应请求的节点并不知道实际数据保存在哪里,所以需要搜索所有节点。Gossip等协议就采用了该方式。
  • 结构化网络:在每个节点中上都维护一份索引,获取数据前先进行检索。
  • 混合模式:建立客户端-服务器模型。各节点作为客户端仍然保存数据,自建服务器专门保存索引,并提供检索功能。

联邦网络架构 #

alt

联邦网络架构属于去中心化的网络架构。

在该架构中,所有节点可以是服务器或Pod。服务器节点订阅Pod。Pod间互相连接共享信息。 Pod主要为了解决P2P网络中难以发现其它节点的问题。如果一些Pod间的联系断开了,Pod上订阅的服务器仍能和其进行通信,其它Pod不会受到影响。

项目分析 #

  • 事前评估
    • 背景评估:为什么要做。
    • 可行性评估:能不能做,值不值得做。
    • 结果评估:要不要做,怎样算成功。
    • 风险评估:人员、时间、技术。
    • 复杂度评估:识别核心功能和难点。
  • 需求澄清:功能性需求和非功能性需求。
  • 技术选型

关键指标 #

  • 可伸缩性/可扩展性(Scalability):
    • 概念:系统在不影响性能的情况下处理不断增加的工作负载(请求量负载、存储量负载)的能力。
    • 衡量方法:系统延迟。即无论系统负载如何变化,延迟都没有变化的情况下,就可以说该系统拥有很好的可扩展性。
    • 扩展方法:
      • 水平扩展 :增加更多的服务器。
      • 垂直扩展:提高每台服务器的硬件性能。
  • 可用性及可靠性:
    • 可靠性(Reliability):系统在出现故障时仍能提供服务。
      • 衡量方法:单位时间内系统正常提供功能的概率。使用平均无故障时间(MTBF)和平均修复时间(MTTR)来衡量。
        • MTBF=(正常功能时间总和-功能异常时间总和)/失败次数
        • MTTR=总修复时间/修复次数
    • 可用性(Availability):系统能够在指定时间内返回响应。响应可以是正常的也可以是错误的,所以可靠的系统一定可用,但可用的系统不一定可靠。
      • 衡量方法:单位时间内系统正常运行的时间百分比,几个9。99.9表示每年宕机时间不大于8.76小时,99.99表示每年宕机时间不大于52.56分钟。
    • 可靠性和可用性主要实现方式系统容错+系统冗余+数据冗余。
  • 性能(Efficiency):系统能同时处理多少请求(吞吐量/带宽)以及每个请求的处理时间(响应时间/延迟)。
  • 可维护性(Servicealility):对系统进行维护的复杂度和时间开销。
    • 可操作性:通过操作可以使系统在故障情况下恢复正常。
    • 清晰性:代码的清晰简单,容易理解和维护。
    • 可修改性:易于为系统添加和修改功能点。

容量评估 #

容量评估前需要先从产品或运营那里获得相关数据规模,如预计新增用户数、上架产品数等。为了简化计算,1024可按1000来计算。

存储量预估 #

常见字段类型的占用空间

  • 数值型:tinyint 1个字节、int 4个字节、bigint 8个字节、decimal(m, d)为 m+2 个字节。
  • 时间型:datetime 8个字节,timestamp 4个字节。
  • 字节流型:blob最大64k,longblob最大4g。

字段数量暂时无法确定时可以按一行记录500字节来估算,此时单表1000W数据占用空间为5G。

缓存量预估 #

按照2-8法则,缓存一般最多存储数据总量的20%。

带宽预估 #

出口带宽 = QPS * 每条数据的占用空间

假设每条数据占用500字节,则QPS为20k时所需出口带宽为10MB/s。

压力测试 #

  • 约束条件:RT、CPU利用率、内存占用、带宽占用、错误率、磁盘I/O利用率等。
  • DID原则:Design(D)设计20倍的容量;Implement(I)实施3倍的容量;Deploy(D)部署1.5倍的容量。

实施步骤

  1. 明确约束条件。压测时只要有一项达到临界值就停止压测。将当前指标作为集群的最大处理能力。
  2. 做好压测数据隔离。对压测流量进行染色,例如在请求头上添加压测标记,通过网关进行分流。
  3. 压测数据入库通常有三种方式:
    • 写入影子库。数据清理方便,应用层需要多使用一套数据库连接池。
    • 写入入不同表。
    • 写入同张表,通过特殊字段区分压测数据和普通数据。
  4. 调用其它服务时注意对不能压测或不用压测的组件做Mock或特殊处理。比如服务调用推荐系统、用户画像、大数据分析等系统时,将压测数据发送到Mock系统而不是真实服务。
  5. 确保下发压测数据的节点贴近用户,为保证真实性,避免和服务器在同一机房。

单机压测

  • 选择一台相当于线上配置的机器进行压测。单机压测实施简单,一般用于项目未上线前。

集群压测

  • 复制集群,对整个集群进行压测,并不断把线上集群的节点摘除,以减少机器数的方式增加线上节点单机的负荷。由于是完全使用线上真实流量进行压测,所以获取的单机最大容量数值更精确。

压测数据的产生

  • 实时流量复制。复制请求同时进行流量清洗,去除无效请求。需要选择流量较大的时候执行。
  • 模拟请求。实现简单,但数据通常无法反应真实情况。
  • 流量工厂重放(日志重放)。将入口流量拷贝一份,批量在请求头添加压测标记,经过流量清洗后保存到NoSQL服务器中,作为流量数据工厂存储。压测时从流量工厂中取数据。流量工厂可以使用GoReplay。

水位标准

项目上线后,根据水位标准(致命线和安全线)来决定是否应该扩容或者缩容。当集群的水位线位于致命线以下时,就需要立即扩容,一般按固定数量或比例进行扩容。当水位线回到安全线以上并保持一段时间后,就可以进行逐步缩容,每次缩容一部分,节省成本。

  • QPS=并发数/平均响应时间
  • 单机能力=单机压测阀值qps
  • 单机负荷=前一天单机最大qps
  • 集群能力=单机能力*集群内机器数
  • 集群负荷=前一天集群最大qps
  • 单机水位=单机负荷/单机能力*100%
  • 集群水位=集群负荷/集群能力*100%
  • 理论机器数=集群负荷实际机器数/(集群能力标准水位),即监控数据/压测数据
  • 机器增加=理论机器数-实际机器数
  • 单机房上限/标准/下限水位为:80%、70%、50%
  • 双机房上限/标准/下限水位为:55%、40%、30%
  • 三机房上限/标准/下限水位为:75%、60%、50%

为了使容量评估更精确,可以采用区间加权来计算,也就是把请求按照响应时间分成多个区间,每个区间分别赋予不同的权重。响应时间越长权重越高,即占用资源越多,比如 0~10ms 区间的权重是 1,10~50ms 区间的权重是 2,50~100ms 区间的权重是 4,100~200ms 区间的权重是 8,200~500ms 区间的权重是 16,500ms 以上的权重是 32。因此单机的最大容量,也就是压测停止时刻采用区间加权方式计算得出。

服务调用设计 #

同步调用 #

  • 优点:所有服务都使用同一数据源,数据一致性可能性更小,可以随时展示最新数据。
  • 缺点:可能会因为被调用的服务发生问题导致级联故障,系统发生大规模雪崩。应用必须进行容错处理。

异步调用-Event传递 #

  • 一次 trigger 产生一次事件,emit 一个通用模型,不同消费者各取所需。
    • 优点:只创建一次事件,事件模型也只有一个。生产者消费者实现简单。
    • 缺点:
      • 违反了领域模型只能在单个有界上下文中有效的原则,部分属性对于某些消费者来说是完全没有用;
      • 通用模型隐藏了依赖关系,任何一次改动都很难掌握是否会对其它消费者产生影响。
  • 一次 trigger 产生多个事件,emit 多个专用事件模型,每个模型包含不同的 EventType 字段。
    • 优点:实现解耦,修改一种业务的模型不会影响到其它业务。
    • 缺点:创建多个事件、多个模型,生产者实现复杂。如果各消费者所需字段区别较小时,也可以使用通用模型。
  • 一个 trigger 产生一次事件,emit ID,由消费者自行决定如何获取具体数据。
    • 优点:只创建一次事件,传递数据量小,对生产者友好。
    • 缺点:为了获取各种数据,各消费者必须了解业务逻辑;不同消费者的获取操作还可能存在重复操作。

服务间数据一致性 #

将校验和(Checksum)和数据一起存储和发送。

服务间感知 #

心跳:向中央服务器或系统中的其它服务器发送心跳消息以表明当前系统仍然存活。

心跳风暴问题:。。。

长轮询 #

  • AJAX定期轮询:客户端定期发起HTTP请求给服务端,服务端返回空或数据。适合单次调用以及不支持其它高级功能的场景。
  • HTTP长轮询:客户端发起长轮询连接,服务端有数据时返回。客户端收到后数据后继续发起新一轮的长轮询。适合简单场景。
  • WebSocket:客户端和服务端在TCP层建立双工通信,双方均可传递数据。适合网游、实时聊天等场景。
  • Server-Sent Events(SSEs):服务端可持续向客户端发送text/event-stream数据。适合Feed流、实时通知等场景。

聚合查询 #

方案一览:

  • A服务调用B服务后,聚合B的结果后返回。
  • 由独立的聚合服务调用A服务和B服务后聚合返回。
  • A服务和B服务将数据同步到聚合服务,由聚合服务返回。
  • 聚合服务通过只读权限跨库查询。

Fallback处理 #

方案一览:

  • 快速失败。
    • 写操作优先采用快速失败。
    • 业务异常优先采用快速失败。
  • 重试。
    • 读操作可以进行重试。
    • 写操作如果要重试必须实现幂等性。
    • 网络异常等非业务异常情况下可以重试。
    • 系统繁忙等异常需要等待一段时间重试。
    • 为避免重试放大引起的系统奔溃,应采用指数退避或者加重试间隔中加入抖动(随机噪声)。

安全机制 #

  • 防止数据篡改:将请求中的参数进行排序后加密获得签名后一起发送。服务端同样将参数排序后加密,校验签名是否一致。
  • 防止重放攻击:参数中加入时间戳后发送,服务端检查时间戳是否在有效的执行时间范围内。也可以请求分两步,客户端先获取token,获取成功后再发送真实数据+token,服务端检查token是否已被使用。
  • 中间人攻击:窃取用户cookie发送给服务端。使用https,将服务端设置cookie为setSecure=true,这样cookie只在https中有效
  • XSS:跨站脚本攻击,用户访问嵌入恶意代码的网站。设置setHttpOnly=true,让JS无法操作cookie,客户端保证任何返回内容都经过escapeHtml操作,过滤掉请求中的存在 XSS 攻击风险的可疑字符串。WebView页面可以注入特殊代码。服务端也可以创建XSSFilter。
  • CSRF :跨站请求伪造,无需拿到用户信息,只要某个用户以登录状态访问网站A后再访问恶意网站,恶意网站引导用户访问网站A。和cookie不同浏览器l不会自动带入token。也可以采用Double-Submit Cookie,执行敏感操作时要求再次登录或输入操作密码。将Token保持在LocalStorage中也可以避免CSRF。

时序问题 #

物理时钟

  • 时钟:基于日历时间,如Java的System.currentTimeMillis()。可能由于计算机中的石英钟的温度等问题发生时间漂移,需要采用NTP同步纠正,但纠正后又可能发生时间跳跃。
  • 单调钟:基于持续时间,如Java的System.nanoTime()。单调钟的开始时间可能是机器启动时间,也可能是任意时间。单调钟保证时间总是前进的,不可回退,NTP同步也只能改变单调钟前进的速率,不会产生时间跳跃。

如果系统依赖时钟,那么当有机器时钟偏移其它时钟太远的话,应该将该节点立即移除。 比如说:机器先写入x=1,ms=2,时钟漂移后再写入x=2,ms=1,如果依赖时间戳则会将x=1认为是最新数据。 可以通过最后写入为准(LWW),Cassandra和Riak中使用这种机制,可以解决并发写冲突,但是无法保证事件的顺序。 更好的解决方法时依赖逻辑时钟(递增计数器、队列排序)而不是物理时钟(时钟和单调钟)。

数据库设计 #

建模方法 #

  • 恩门建模:自顶向下,从数据源开始。基于关系构建,冗余数据少,适合金融等应用场景固定的业务。
    • 例:买家、商品是实体,买家购买商品是关系,所以DB应有买家表、商品表、买家商品交易表。
  • 金博尔建模:自底向上,从数据分析需求开始,关心事实在不同维度下的结果。冗余数据较多,适合快速变化的互联网业务。
    • 例:用户、商品是维度,库存和账号余额是事实,所以DB应有用户维度表、商品维度表、账号余额事实表、商品库存事实表。可以在账号余额事实表中添加商品ID来同时分析交易金额和账号余额。

缓存设计 #

  • 移动端本地缓存
  • 浏览器缓存
  • CDN:依赖CDN服务商提供更靠近客户端地理位置的缓存数据。由于属于不同域名的第三方服务,可以解决多个同源请求引起的性能问题。
  • 代理服务器缓存
  • 应用服务器缓存:数据直接缓存在本地磁盘或内存中,每台服务器的缓存数据各自独立。
  • 分布式缓存:依赖外部存储实现全局缓存。

高可用设计 #

容错方案 #

  • 超时
  • 快速失败:检查异常或业务异常通常无需重试,直接抛错或默认值。
  • 隔离:进程级,线程级等。一般将耗时长的请求放到单独线程池中执行,这样即使线程池耗尽也不会影响到其它功能。
  • 降级
    • 页面降级:按钮置灰、功能禁用;动态页面改为静态内容;
    • 延迟处理:定时任务延后处理;实时任务入队列;
    • 读写降级:禁止读/写操作;
    • 准确性降级:使用缓存替代数据库;返回以前保存的数据;
    • 接口降级:快速失败;调用备用逻辑;
  • 熔断(断路器)
  • 限流

FMEA #

FMEA 方法主要用于分析故障模式和影响。

实施方法

  1. 给出初始的架构设计图
  2. 假设架构中某一部件发生故障
  3. 分析此故障对系统功能造成的影响
  4. 制作FMEA分析表,根据分析结果,判断是否可进行优化

FMEA 分析表

包含以下内容,各指标应尽量包含具体数字。

  1. 功能点:从用户角度而非技术组件角度,如注册是功能点而Redis不是
  2. 故障模式:包括故障点和故障形式。量化现象无需原因,如MySQL响应慢到3秒。
  3. 故障影响:量化故障影响,如20%用户不可用。
  4. 严重程度:致命、高、中、低、无
  5. 故障原因:
  6. 故障概率:高、中、低
  7. 风险程度:风险程度 = 严重程度 × 故障概率
  8. 已有措施:告警、自恢复、容错等
  9. 规避措施:技术手段、管理手段
  10. 解决措施:无法解决的才采用规避措施
  11. 后续规划:

实例

alt

多机房部署 #

1、同城双活

A机房联通,B机房电信,机房之间专线连接。核心思想是系统数据尽可能同机房读写,但也支持跨机房写入。

  • 主库:放在机房A中。A机房和B机房的数据都会写入A机房中。
  • 从库:机房A和机房B各放一个。查询请求优先查询本机房的从库。
  • 主从切换:机房A故障后机房B的从库提升为主库。
  • 缓存:优先查同机房的缓存,未命中则查询同机房的从库。更新时同时写双机房的缓存以缓解主从同步的延迟。
  • 服务注册:注册中心可以部署在单机房。服务注册时使用"机房名+服务名"作为注册名,不同机房的服务组成不同的服务组。每个机房的服务只订阅当前服务组的接口。

2、异地多活

异地多活机房间距离应该比较远,如上海和北京,此时同城双活的跨机房写数据库方案就不适用了。核心思想是读写同机房,跨机房异步同步。

  • 数据只写入本机房数据库,然后同步到异地机房。实现方式如下:
    • 数据库采用主从复制,一个机房主库,异地机房从库。
    • 基于消息队列同步。
  • 用户基于地理位置分片,读写都在同一机房。

Devops #

灰度发布 #

  • 金丝雀发布:先发布几台,正常后将剩余的一起发布
  • 滚动发布:先发布几台,正常后再发布下一批次
  • 双服务器组蓝绿发布:在新节点发布新版本,然后一次性切换
  • 双服务器组蓝绿金丝雀发布:在新节点发布新版本,先切换几台,正常后一起切换
  • 双服务器组蓝绿滚动发布
  • 功能开发发布:开关打开后走新逻辑
  • A/B发布:将一定用户切换到新版本,正常后全部切换

监控系统 #

监控指标

QPS、PV、RT、错误率、CPU利用率、CPU负载、内存占用率、硬盘空间等。

监控工具

  • 日志监控:ELK
  • 调用链监控:CAT、Skywalking
  • 指标监控:Promethus+Gafana
  • 业务监控:自行开发+Gafana

项目上线自查手册 #

SQL语句

  • 表关联数
  • 是否包含复杂条件
  • 每次返回行数
  • 一天执行次数
  • 是否使用缓存

后台任务

  • 幂等性
  • 是否支持自恢复
  • 执行频率
  • 每次数据量
  • 每次执行时间

服务调用

  • 幂等性
  • 依赖的服务不可用时

消息

  • 幂等性
  • 发送失败重试
  • 消费失败重试

缓存

  • 缓存占用空间
  • 有效期

安全性

  • 敏感信息存储和展示
  • 数据传递安全性
  • 日志脱敏
  • 密钥管理方案

版本升级

  • 老数据在新服务上
  • 新数据在老服务上
沪ICP备17055033号-2