【可伸缩服务架构】框架与中间件

虽然这是一本老书,但涉及了分布式系统中常见的组件设计思路,其中的架构设计思想,是不会过时的。


更新历史

  • 2022.03.17:完成初稿

读后感

这本书其实不是第一次打开了,但是之前对于分布式系统的理解不够深入,导致更多是囫囵吞枣,没有办法很好地领会关键的设计思想。经过几年的历练,再次打开这本老书,读起来轻松了,能去粗取精抓住重点了。比较可惜的是书本里主要用的是 Dubbo 实现,和我的技术栈不太匹配。不过问题不大,重在思想!

读书笔记

永不重复的高性能分布式发号器

  • 发号器作为分布式服务化系统不可获取的基础设施之一,在保证系统正确运行和高可用上发挥着不可替代的作用
  • UUID 虽然能够保证 ID 的唯一性,但无法满足业务系统需要的很多其他特性,如时间粗略有序性、可反解和可制造性。
    • 注:但是对于日志来说,UUID 是足够的
  • 分布式系统对发号器的基本需求
    • 全局唯一
    • 粗略有序(秒级有序、毫秒级有序)
    • 可反解,即本身含有一定信息量
    • 可制造
    • 高性能,单台机器 TPS 能达到 10000
    • 高可用
    • 可伸缩
  • 基于比特设计,可以用 64 个 bit 实现全部的需求,对比 UUID 更省空间
  • 运行发号器需要保证时间的正确性,可以定期执行 ntpdate -u pool.utp.orgpool.ntp.org 进行同步

可灵活扩展的消息队列框架

  • 消息队列在互联网领域里得到了广泛应用,多应用于异步处理、模块之间的解耦和高并发系统的削峰等场景
  • 这部分更多是客户端的具体实现,略过

轻量级的数据库分库分表架构

  • 垂直拆分:根据业务的维度,将原本的一个库(表)拆分为多个库(表),每个库(表)与原有的结构不同
  • 水平拆分:根据分片(sharding)算法,将一个库(表)拆分为多个库(表),每个库(表)依旧保留原有的架构
  • 在 MySQL 的表中达到千万级别,就需要考虑进行分表
  • 三种解决方案
    • 客户端分片
      • 应用层直接实现:通用、简单,但对业务有侵入,不过性能更高,实现简单
      • 定制 JDBC 协议实现:不侵入业务,但开发人员需要理解 JDBC 协议
      • 定制 ORM 框架
    • 代理分片:对业务无侵入,但增加了代理层,有性能损失
    • 分布式数据库,如 OceanBase, TiDB 等
  • 分库分表引起的问题
    • 扩容与迁移:一般成倍扩容,方案较复杂
    • 查询问题:非主键字段,比较难查询
      • 多个分片表查询后合并数据集,效率很低
      • 使用搜索引擎,数据冗余,查询和存储分离
    • 跨库事务难以实现
    • 同组数据跨库问题

缓存的本质和使用

  • 我们在使用缓存提高读操作性能的同时,一定会失去部分的一致性
  • 适合使用缓存的场景
    • 读密集
    • 存在热数据
    • 对响应时效要求高
    • 对一致性要求不严格
    • 需要实现分布式锁
  • 不适合使用缓存的场景
    • 读少
    • 更新频繁
    • 对一致性要求严格
  • 应用层访问缓存的模式
    • 双读双写:读操作先读缓存,写操作先写数据库
    • 异步更新:全量数据保存在缓存中,并且不设置过期时间,由异步的更新服务将数据库里的变更同步到缓存中;机制复杂,但性能较好
    • 串联模式:在微服务中不推荐,要保证高可用成本较高
  • 分布式缓存分片的三种模式
    • 客户端分片:性能较好,但是对业务有侵入
    • 代理分片:增加了代理层,增加损耗和维护成本
    • 集群分片:如 Redis 3.0 的 Cluster
  • 分布式缓存的迁移方案
    • 平滑迁移:双写方案 - 双写、迁移历史数据、切读、下双写
    • 停机迁移
    • 一致性哈希
  • 缓存设计的核心要素
    • 容量规划:缓存内容的大小、缓存内容的数量、淘汰策略、缓存的数据结构、每秒的读峰值、每秒的写峰值
    • 性能优化:线程模型、预热方法、缓存分片、冷热数据的比例
    • 高可用:复制模型、失效转移、持久策略、缓存重建
    • 缓存监控:缓存服务监控、缓存容量监控、缓存请求监控、缓存响应时间监控
    • 注意事项:缓存穿透、大对象、是否使用缓存实现分布式锁、是否使用缓存支持的脚本、是否避免了竞争条件、缓存雪崩的预防
  • 缓存设计的优秀实践
    • 对应用需要缓存的数据大小进行评估,包括缓存的数据结构、缓存大小、缓存数量、缓存的失效实践,然后根据业务情况自行推算在未来一定时间内的容量的使用情况
    • 将使用缓存的业务进行分离,核心业务和非核心业务使用不同的缓存实例,从物理上进行隔离
    • 根据缓存实例提供的内存大小推算应用需要使用的缓存实例数量
    • 缓存的超时时间的设置是很重要的
    • 所有的缓存实例都需要添加监控,我们需要对慢查询、大对象、内存使用情况做可靠的监控
    • 不推荐多个业务共享一个缓存实例,不得不共享时,需要通过规范来限制各个应用使用的 key 有唯一前缀,避免相互覆盖
    • 任何缓存的 key 都必须设定缓存失效实践,且失效时间不能集中在某一点
    • 低频访问的数据不要放在缓存中
    • 缓存数据不易过大
    • 对于存储较多 value 的 key,尽量不要使用 HGATALL 等集合操作
    • 对性能要求不是非常高,尽量使用分布式缓存,而不要使用本地缓存
    • 写缓存时一定要写入完全正确的数据
    • 使用缓存是,一定要有降级处理,尤其对关键的业务环节,缓存有问题或者失效时也要能回源到数据库进行处理

RPC 服务

  • RPC 协议是一种通过网络向远程计算机程序请求服务,而不需要了解底层网络技术的协议。优势:简单、高效、通用
  • RPC 协议以传输层协议(如 TCP, UDP, HTTP)为基础,为两个不同的应用程序间传递数据
  • RPC 采用客户端/服务端模式
  • 网络上的两个程序通过一个双向的通信连接实现数据的交换,这个连接的一端被称为 Socket。Socket 用于描述 IP 地址和端口,是一个通信连接的句柄,可以用来实现不同的计算机之间的通信,是网络编程接口的具体实现
  • 实现透明的远程过程调用的重点是创建客户存根(client stub)