Subsections of Distribute Service

后端存储以及存储服务

把后端存储当成一个贯穿很多知识点的 Topic,这样可以把相关的知识点串联起来,形成一个完整的知识体系。

后端存储是分布式服务中最重要的一部分。存储承担了数据的持久化、读写、查询等功能,一个安全可靠、快速稳定的存储基础设施是构建稳定的业务系统的基础。

根据不同的业务场景,选择使用不同的存储技术,是一种能力。

MySQL、Redis、ElasticSearch、HBase、Hive、MongoDB、RocksDB、CockroachDB 等等,这些存储还真就是谁都替代不了谁,每一种都有它擅长的地方,有它适用的场景,当然也有很突出的短板。如何根据业务系统的特点,选择合适的存储来构建我们的系统,是需要学习和掌握的。

目录列表

TODOs

  • 分库分表
    • ShardingSphere JDBC

Subsections of 后端存储以及存储服务

缓存设计

互联网服务中,缓存是提高系统性能的重要手段。缓存的设计需要考虑的因素很多,包括缓存的类型、缓存的位置、缓存的更新策略、缓存的一致性等等。本文将从这些方面来介绍缓存的设计。

缓存

缓存,是一种存储数据的组件,它的作用是让对数据的请求更快地返回。实际上,凡是位于速度相差较大的两种硬件之间,用于协调两者数据传输速度差异的结构,均可称之为缓存。

技术为业务服务,缓存也不例外;在业务系统中,主要会用到以下几种缓存:

  • 本地缓存 (aka 进程内缓存)
  • 分布式缓存
  • 类似于 CDN 的静态数据缓存

进程内缓存

一般也叫 Local Cache, 是指缓存在应用进程内部的缓存,比如 Java 中的 HashMapConcurrentHashMap 等。这种缓存的特点是速度快,但是缓存的数据量有限,一般只能缓存一些热点数据;此外,这类缓存会引入多实例下数据短暂不一致的问题,所以在使用的时候要格外小心。

经常使用的 Local cache 有:Google Guava CacheCaffeineConcurrentHashMapEhcache 等。目前 Caffeine 是性能最好的 Local Cache。此外,leveldb 也是一种本地缓存的实现,但是它是基于磁盘的,所以性能不如上面几种。

什么时候需要用本地缓存?什么场景不适合用?

举几个场景

  • 黑名单用户 ID / 白名单用户 ID;这种数据量不大,但是访问频率很高,所以可以用本地缓存,在启动时加载,运行过程中随着 Admin 的添加或者删除而更新
  • 用户 ID 和 用户名、头像、昵称等的对应关系,由于某些原因我们一般指存储用户的 ID,当需要用户的其他信息时就需要从数据库中查询这些数据回来,如果每次都从 DB 中获取,显然对 DB 的压力很大,并且每次访问 DB 不是很有必要
  • 分布式配置中心,为了降低业务服务对配置中心的压力,一般都是将数据缓存在本地,当有新的配置发布后,会通知业务服务中配置中心的 client,然后拉取新的配置,或者推送新的配置

总结一下

  1. 数据读多写少且对一致性要求不高的场景;如果一致性要求非常高,需要考虑使用分布式缓存,并且还要考虑数据库与分布式缓存的一致性问题
  2. 对性能要求极其严苛的场景,比如秒杀、抢购等
  3. 只读数据的场景
  4. 分布式缓存的普适性更强一些,很多场景都可以用分布式缓存来解决,如非必要,不建议使用本地缓存 (也就是说用了也要慎重)

优势是什么?劣势是什么?

Local Cache 和应用服务处于同一个进程内,且存储在内存中,一个字:快;无论对于 RT、QPS 还是吞吐量,都有极大增益。此外,也节省了内网带宽。

依据服务无状态设计的原则:尽量把数据的状态和存储放在专门的数据存储服务中,实例节点本身只做计算,不把数据状态和某个实例做耦合,这样在水平扩展时才能收放自如。本地缓存其实在一定程度上引入了状态,所以一个最需要关心的问题就是数据一致性,这一点有别于分布式缓存。

  • 本地缓存:每个实例都有一份缓存,需要协调数据一致性问题
  • 分布式缓存:缓存数据集中存放在独立的缓存服务中,每个实例是不存在缓存数据的,但是需要考虑缓存和 SOR (Source of Record) 的一致性问题

目前有哪些本地缓存的实现方案,各自的优缺点?

  • 编程语言的内置数据结构,比如 Java 中的 HashMapConcurrentHashMap
  • Google guava
  • Caffeine
  • Encache
  • Leveldb / Rocksdb

本地缓存关注的技术点是什么?

  • 缓存命中率
  • 缓存容量
  • 缓存淘汰策略
  • 缓存一致性

分布式缓存

TODO

缓存规范

  • JSR107
  • Spring Cache

缓存实践

缓存可以应对高并发大流量的场景。

在做缓存设计时,有一些误区要避开:

  1. 把缓存作为服务间传递数据的通道,这个要避免
  2. 使用缓存没有考虑雪崩的情况,这个要考虑
  3. 多服务共享一个缓存实例,这个最好要避免,多个服务之间缓存做垂直拆分

找几个例子分析一下


References

Frameworks

https://bbs.huaweicloud.com/blogs/365239 https://mp.weixin.qq.com/s/36kVm4pfiy2vQEMToZ9--g https://cloud.tencent.com/developer/article/1546995 https://juejin.cn/post/6844903961078530062 https://mp.weixin.qq.com/s/YBpOz1dQ0sG15vGL7N0PeQ https://shniu.gitbook.io/cs/middleware/redis https://shniu.gitbook.io/cs/system-design/backend-store/cache-design https://tech.meituan.com/2017/03/17/cache-about.html http://highscalability.com/blog/2016/1/25/design-of-a-modern-cache.html https://juejin.cn/post/7151937376578142216 https://juejin.cn/column/7140852038258147358

BFF

Backend for Frontend 是一种模式,在构建 API 时当然可以不选择使用 BFF,取而代之的是对多个 UI 终端提供一个统一的 API,但是不可避免的会带来一些问题:

  1. 不同的 UI 终端展示的数据是不一样的,比如 Web H5 和 App 之间会存在较大的差异
  2. 不同的 UI 终端可能会有不同的逻辑,比如有些数据是 Web 用,有些则是 App 用,这样都放在一个接口里的话,对不同的终端来说,都会有一些无用的数据,或者是无用的逻辑,这样会导致接口的复杂度增加,维护成本增加;从节约流量和资源的角度说,移动端应该尽可能少的去拉取数据
  3. 统一的 API 在开发新功能时,可能会对不同 UI 终端都产生影响,而且功能逻辑会相互耦合;也就是说一个接口承担了更多的职责

总结来看:由于不同终端的差异性,导致对后端 API 的数据产生了不同的需求,驱动我们去考虑针对不同的 UI 来定制不同的接口。

通用的 API 后端和团队组成如下

通用 API 后端 通用 API 后端 通用 API 团队组成 通用 API 团队组成

使用 BFF

The goal of the BFF pattern is to decouple the front-end applications from the back-end services and to reuse the APIs while ensuring that it doesn’t cause over-fetching or over-requesting on the client-side. This is accomplished by developing a dedicated back-end for each front-end service. The BFF transforms the data into the correct format that the client application needs.

BFF Overview BFF Overview

  1. BFF 的接口通常是由前端开发人员来定义,由后端开发人员来实现
  2. BFF API 一般以 UI 为依据,这样更集中
  3. 至于是不是必须为每个类型的终端都定义不同的 BFF,这个需要根据实际情况来判断,或者团队来决定采用何种模式;比如 iOS 和 Android 的 UI 体验几乎完全一致,我们有必要指提供一套 BFF,而不是一个 iOS BFF 和一个 Android BFF;所以具体情况还要具体分析,没有一个标准答案
  4. 在调用多个下游服务时,使用 RxJava 或 Finagle 这类响应式编程风格的方式会更加的易于管理,而且可以更好的控制并发度,提高性能
  5. 在使用 BFF 时带来的最大困扰是重复代码,这个可能无法避免,但是在合理的情况下可以考虑复用代码、抽象代码达到消除重复的目的
  6. 一个指导规则可能是:如果被提取的功能不必同时更新,则使用共享库,但如果需要,则使用服务。
  7. 谈论复制和重用,不要试图从一开始就让一切通用。如果你尝试,并且它在整个组织中使用,就会引起摩擦,因为很多人都想做出贡献。在考虑通用用法之前,请专注于您的功能和特定用例。“特殊功能优先于通用用法”策略效果更好。在考虑提取代码时,不要忘记有用的“三法则”。

关于 BFF 的实践可以看一下 BFF @ SoundCloud 这篇文章,里面有一些很好的实践经验。

什么时候用 BFF ? 当你有多个微服务,并且需要应对不同的 UI 终端,而且这些终端之间存在差异性,可以考虑使用 BFF 模式。

Event-driven BFF

AWS 提出了一个 event-driven BFF 的东西: Backends for Frontends Pattern by AWS

AWS event driven BFF AWS event driven BFF

  • 右边是微服务或单体服务发出的 event,这些 event 在 Domain 发生更新时发出
  • 中间是 Event bus,接收 event 后转发给左边的 BFF 层的订阅者
  • 左边是 BFF 的实现,不同的 BFF 可以有不同的数据库投影,根据具体的需求来定制数据结构,当然也可以支持 CDC 模式

还有一种更加云原生的方式来构建服务

AWS event driven BFF AWS event driven BFF

Reference