分布式系统应用

65 阅读11分钟

Redis和MySQL的一致性

缓存设计

  • 旁路缓存模式 读:先读redis,没有再读数据库然后写入redis 写:先写数据库,再删除redis , 这种写入方式叫做 Write-Around(不写缓存)

  • 读写穿透模式:读穿透+写穿透,抽象出缓存层,业务系统不直接和缓存交互,而是和缓冲层交互,缓存层在和具体的数据库(redis or mysql)交互

    读:先读redis,没有再读数据库然后更新redis 写:先更新redis,再更新数据库(通过事务保证原子性)

  • 异步写入模式(也称为写后模式 write-behind) 读:先读缓存 写:缓存中数据达到一定量级后,再批量同步到数据库

缓存的加载模式

系统启动时何时加载缓存

  • 预刷新:提前预热系统,将数据库中数据加载到缓存
  • 懒加载:具体请求时再加载

一致性分析 主要是读/写缓存和数据库这两个操作不具备原子性的问题:在多线程环境下如何保证一致性,以及某个操作出现错误然后影响一致性

  • 先写缓存再写数据库 写数据库报错就会出现不一致 多线程下同时写也会出现问题
  • 先写库再写缓存 报错回滚数据库,也许能解决不用回滚缓存的问题 但是多线程下同时写会出现问题
  • 先删除缓存再删除数据库 考虑多环境下读写:A删除->B读取->B更新缓存->A写数据库==>不一致
  • 先写库再删除缓存 多环境下读写(考虑缓存过期):缓存过期->A读取数据库->B写数据库->B删除缓存->A写缓存==>不一致

解决方案

  • 失败重试策略:先写库再删除缓存,删除失败就重试删除
  • 延迟双删除策略:先删除缓存,写库,隔一段时间(200ms-300ms)再次删除缓存(还是要解决保证删除成功)
  • 异步更新/删除策略:解决代码侵入性高,耦合度高 删除缓存->更新数据库->发生删除缓存MQ->消费者删除缓存 可以使用canel监听bin-log实现删除缓存策略,几乎0侵入。
  • 更新标识策略:用于表示当前是否在处于更新阶段,处于更新阶段从数据库读取到的数据就不放入缓存,对业务侵入性比较高

分布式ID

分布式组件

为什么需要这样组件?没有这个会怎么样,有这个会怎么样

注册中心

什么是注册中心:服务提供方在注册中心注册自己的服务,服务调用方更具自己的需要从注册中心拉取提供服务的列表,然后调用提供方提供的服务。

为什么需要注册中心?

  • 没有注册中心:系统间调用,ip采用硬编码方式,扩展性差,耦合性高
  • 有注册中心 降低后合度:子系统迁移,依赖它的系统不需要修改代码 提高维护性 检查节点健康状态:能够很大程度上保证通信顺畅 支持动态伸缩:集群扩展方便

注册中心的优点

  • 服务注册
  • 服务发现
  • 系统解耦
  • 灵活:节点动态伸缩,应对突发状况
  • 中心化管控:健康探测,自动剔除故障节点
  • 扩展性

市面上的解决方案:Zookeeper,Eureka,Consul,Nacos

配置中心

用于统一的管理配置(服务越来越多,配置也会越来越难以管理)

**约定大于配置:**如约定好的项目结构,减少不必要的沟通。

**配置大于编码:**能通过编码实现就不要用硬编码。

配置中心的好处

  • 集中管理
  • 降低冗余
  • 动态刷新
  • 版本管理
  • 多环境支持

远程调用

服务间的相互调用过程类似,可以抽取成为一个组件

解决方案:OpenFeign,Dubbo

好处

  • 无需关心调用底层协议细节:直接调用即可,不管是Http还是其他协议
  • 使用起来类似本地调用
  • 与注册中心配合:从配置中心感知调用地址

服务网关

网关:为系统提供一个统一的外部API入口,主要功能就是路由,将外部请求路由到具体的节点

网关分类

  • 接入层网关
  • 服务网关

服务网关功能

  • 请求路由:承接外部流量,按照规则转发
  • 安全控制:统一鉴权,反爬虫等
  • 响应处理:对响应统一处理
  • 协议转换:转换外部协议和内部协议
  • 负载均衡:
  • 流量控制:控制流量,保护节点安全

为什么需要网关?

  • 将大多数请求的通用能力进行一个统一实现

服务网关的解决方案:Zuul,GateWay

负载均衡

用于服务间调用或者网关往集群分发请求的节点选择策略

分类

  • 服务端负载均衡:统一的负载均衡服务,通过调用这个服务然后再负载均衡到具体的服务,如nginx可以充当这个角色
  • 客户端负载均衡:将负载均衡在发起调用的一方实现,与注册中心紧密配合

为什么不用服务端负载均衡?

  • 太重了,中心化
  • 客户端负载均衡让每个客户端都局部分发能力,提供容错性

客户端负载均衡的缺点

  • 会出现请求倾斜

市面上客户端解决方案:Ribbon,LoadBalancer

服务保护

集群的弹性伸缩在一定程度上能够解决高并发带来的问题,但是硬件的资源始终是有限的,一个节点的故障有可能会影响整个集群。

服务雪崩:单个服务的故障蔓延到整个系统

**服务降级:**某个服务不可用时切换到备用服务或者方案

服务保护组件:保证系统在故障不可控环境下稳定运行

  • 防止雪崩发生
  • 实现降级和限流
  • 资源隔离:如不同的接口使用不同的线程池,从而这个接口的并发不影响其他接口的线程资源
  • 请求缓存、合并、重试和监控机制

解决方案:Hystrix,sentinel等

分布式事务

一个请求涉及多个库的操作,需要分布式事务进行控制。

解决方案:seata

持续集成/持续部署(CI/CD)

分布式下节点过多,部署麻烦

常见方案:Git+Jenkins+Docker+Kubernets

日志收集

解决分布式下日志分散在各个应用内

解决方案:ELK等

链路追踪

从统一的日志管理中查找对应日志链路从而分析bug问题。

为每个请求分发一个全局ID,输出日志,携带输出ID

系统监控

监控服务器资源,应用资源,中间件监控,业务监控等这些。

微服务设计

微服务等于分布式系统,但是分布式系统不等于微服务

为什么需要要微服务?系统规模大,用户体量大,流量峰值够高,项目成员多,硬件资源够,工期时间足

  • 所有成员在一个项目上开发,代码不好管理
  • 业务耦合度高
  • 吞吐量

设计原则

设计要求:

  • 高内聚:每个服务内部功能高度相关,专注单一业务
  • 低耦合:服务间依赖最小化,减少变更带来的影响
  • 可靠性:某个服务故障时,能保证其他无关服务的运行
  • 稳定性:减少事故发生频率
  • 可用性:系统提供服务的时间比例
  • 可扩展性:能够按需扩展资源应对负载变化,也可以是功能上的扩展不影响其他功能的
  • 灵活性:服务能够适应需求变化和技术变化的能力,如替换成新服务
  • 可维护性:系统易于理解,修改,升级。
  • 独立性:团队独立,数据独立等
  • 可管理性:系统监控,配置管理等

设计准则:

  • 单一职责
  • 隔离性:如有自己独立数据库
  • 可伸缩性:能够水平扩展
  • 可替换性:替换成新服务,依旧能运行
  • 容错性:部分失败不影响全局,需要有备用服务和故障处理
  • 开闭原则
  • 服务自治性:每个服务都包含自己的业务逻辑和数据
  • 可观察性:能够监控
  • 可重复部署:能够重复的部署

设计原则

  • 完善的基础设施:包含所有的分布式组件,不要等出问题再去考虑
  • 拆分粒度:考虑业务,流量,团队,适当拆分
  • 服务分层:服务又基础服务和业务服务两大类。分层就是需要抽取服务里的共性操作,抽象成独立的基础服务,基础服务会被大量的其他服务接入,稳定性是首要考虑的问题。
  • 无状态设计:要能够弹性伸缩,前提就是无状态服务,如定时调度技术SpringTask会使得服务有状态,伸缩都需要去考虑其带来的问题。Session的使用也是一样。
  • 容灾体系:做好容灾方案,如数据备份,主从热备,容灾告警,多机房等。中小型系统也没有必要做到面面俱到,因为容灾资源99%的时间都是闲置状态。
  • 约定大于一切,配置大于编码:做好编码,数据库,性能规范等管理。微服务开发最好是接口先行,再考虑语言实现。
  • 面向失败设计:尽量去考虑出现失败后的场景,设计好兜底方案。
  • 摆脱单体思维:合理运用中间件处理业务,如redis,MQ,ES等去实现业务。

微服务祛魅

微服务并非银弹,其带来了更高的灵活性的同时也带来了复杂性和其他许多挑战。

如果说目前项目的还没有使用微服务的必要,但是又需要考虑到未来用户体量增大时去进行服务拆分的场景,那么尽量先通过Maven的多模块去构建单体项目。

服务过度拆分

每个服务都应该由一个团队负责,过度拆分会提高人力成本,同时也可能会降低沟通的效率,从而降低了开发效率。

盲目跟风微服务

微服务有很多的好处,但不是所有应用所有场景都要用到微服务,盲目使用只会提高成本。

大部分情况用微服务的原因都是:汇报更好看,对外更好吹,提升开发者的竞争力。

更低的性能

服务之间的调用需要通过网络完成,相比于原来的直接本地调用,性能只会更差。这个性能之间的差距往往是数倍的关系。

微服务接口设计

怎么设计接口

  • 完善注释和接口文档
  • 合理的执行日志:位置,级别,内容都要合理 日志链路跟踪
  • 统一的接口风格: RESTful风格 统一的返回结构
  • 跨域解决
  • 安全性设计 鉴权机制 接口签名机制:发放token 出入数据加密:防止篡改和请求重放,对响应进行加密(防止爬虫) 黑白名单机制:ip级别,用户界别,地域级别
  • 资源隔离:如线程池隔离
  • 版本兼容问题:一定要考虑
  • 支持动态配置:熔断时间动态配置
  • 面向失败设计:考虑好所有失败的情况
  • 做好接口限流

如何写好接口

  • 处理好写入资源:写到哪里?要不要保留
  • 请求排队:单次调用接口消耗大量资源,不用进行实时处理,就可以需要进行排队
  • 接口防止重复和幂等设计
  • 批处理思想:合并读写
  • 多线程优化:异步优化或者并发优化
  • 并发安全机制:分布式锁这些

接口防重和幂等

重复请求:用户重复提交,非法重复提交,失败重试,重复消息,网络故障

幂等机制:多次调用接口返回结果一致

防重解决方案

  • 前端 按钮变成灰色防止重复点 重定向页面
  • 后端 **使用唯一key:**获取key,首次提交生成放入redis **防重表:**操作往表插入操作id **状态机:**操作前检查操作状态(如订单状态) **Token方案:**进入表单先获取唯一Token,提交时携带token,后台删除redis中的token,没有检查到token说明重复提交了