Dunwu Blog

大道至简,知易行难

Cinchcast 的架构

Cinchcast 提供的解决方案允许公司创建、共享、衡量和货币化音频内容,以接触和吸引对其业务最重要的人。我们的技术将会议桥接器与实时音频流相结合,以简化在线活动并增强参与者的参与度。 Cinchcast 技术还用于为全球最大的音频社交网络 Blogtalkradio 提供动力。今天,我们的平台每天制作和分发超过 1,500 小时的原创内容。在本文中,我们描述了我们为扩展平台以支持这种规模的数据而做出的工程决策。

统计数据

  • 浏览量每月超过 5000 万
  • 创建了 50000 小时的音频内容
  • 1500 万个流媒体
  • 175,000,000 次广告展示
  • 峰值每秒 40000 并发请求
  • MSSQL、Redis、ElasticSearch 集群中存储的数据达到每天数 TB,
  • 10 人工程师团队
  • 生产环境大概有 100 左右的硬件节点

数据中心

线上网站部署在布鲁克林的数据中心。但 QA 和 Staging 环境则使用了 Amazon EC2 云实例。

——考虑到数据安全,大部分公司不愿意把真实数据部署在云端。

硬件

  • 大概有 50 台 Web 服务器
  • 15 台 MS SQL 数据库服务器
  • 2 台 Redis 的 NoSQL 的键值服务器
  • 2 台 NodeJS 服务器
  • 2 台 弹性搜索集群服务器

开发工具

  • NET 4 C#:ASP.NET 和 MVC3
  • IDE 用的是 Visual Studio 2010 Team Suite
  • 用 StyleCop、ReSharper 来强化代码标准
  • 使用敏捷。其中大的功能用 Scrum,小任务则通过看板任务墙管理
  • 测试和持续集成使用 Jenkins + Nunit
  • 自动化测试则是 Selenium 和 Sauce On Demand

软件和使用的技术

  • Windows Server 2008 R2 的 64 位操作系统
  • 基于微软 Windows Server 2008 Web 服务器下运行的 SQL Server 2005
  • 负载均衡是 EQL(Equalizer load balancers)
  • Redis 作为分布式缓存层和消息分发队列
  • NodeJS 用来进行实时分析和更新仪表盘
  • 搜索用得是 ElasticSearch,日志分析是通过 Sawmill+自定义分析器脚本

监测

  • NewRelic:性能监控
  • 性能对 KPI(转换率,页面浏览量)的影响:Chartbeat:
  • Gomez,WhatsupGold,Nagios 等用来各种预警和报警
  • SQL Server monitoring 的监控:来自 Red Gate 的 SQL Monitor

我们的原则

  • 尊重他人的时间。不要带着问题来,要拿出解决办法。
  • 不要去追逐当下的热点技术,先实现基本功能,然后再做锦上添花的。务实是最重要的。
  • 成为一个“如何做”的团队而不是总是说“不”的团队
  • 预先处理总比亡羊补牢要好,把安全植入到软件开发生命周期中,通过培训开发人员如何写出安全的软件并把它从一开始就作为业务优先考虑之处。

架构

  • 所有 Javascript、CSS 和图像都缓存在 CDN 级别。 DNS 指向一个 CDN,它将请求传递给源服务器。我们使用 Cotendo 是因为它允许在 CDN 上做出 L7 路由决策。
  • 单独的 Web 服务器集群用于为普通用户和广告用户的请求提供服务,通过 cookie 进行区分。
  • 我们正在转向面向服务的架构,其中系统的关键部分,例如搜索、身份验证、缓存,都是以各种语言实现的 RESTFUL 服务。这些服务还提供了一个缓存层。
  • REDIS NOSQL 键值存储(redis.io)用作数据库调用之前的缓存层。
  • Scaleout 用于在网络服务器集群中维护会话状态。但是,我们正在考虑切换到 REDIS。

经验教训

  • SQL Server 数据库中的文本搜索不好用,经常出现 CPU 阻塞,所以 Cinchcast 切换到 ElasticSearch,一个 Lucene 的衍生工具。
  • 微软内置的会话模块容易出现死锁,他们用 AngiesList 会话模块取代了它,并把数据存储到 Redis。
  • 日志是发现问题的关键。
  • 重新发明轮子,有时候也可以是一件好事。例如,在一个供应商的提供的 JS / CSS 的产品导致性能问题的时候,他们通过重写显著改善了网站的性能。
  • 并不是所有的数据都是关系型的。
  • 在开发中不使用指标检测就像在风暴中不参考高度表来降落飞机,因此整个开发过程中,一定要通过网站吞吐量,解决错误的时间、代码覆盖率,等指标来衡量你的效率。 总的来说,对于日 PV 百万级的网站来说,Cinchcast 的架构、研发、运维等层面的技术选型和经验值得学习和参考。

参考资料

亚马逊的架构

摘录的要点

可扩展:添加资源,性能成正比提升

分布式、去中心化

隔离性:面向服务,聚合数以百计的服务,对外统一提供服务

同时支持 REST 和 SOAP

团队在精不在多,节省沟通成本

状态管理是大规模系统的核心问题,如分布式 Session 等

设计应尽量简单,很多问题可以用业务逻辑去解决,而不是通过技术

参考资料

设计 Pastebin.com (或者 Bit.ly)

本文搬运自 设计 Pastebin.com (或者 Bit.ly)

注意: 为了避免重复,当前文档会直接链接到系统设计主题的相关区域,请参考链接内容以获得综合的讨论点、权衡和替代方案。

设计 Bit.ly - 是一个类似的问题,区别是 pastebin 需要存储的是 paste 的内容,而不是原始的未短化的 url。

步骤一、需求分析

收集这个问题的需求和范畴。
问相关问题来明确用例和约束。
讨论一些假设。

用例

问题范围

  • 用户 输入一段文本,然后得到一个随机生成的链接
    • 过期设置
      • 默认的设置是不会过期的
      • 可以选择设置一个过期的时间
  • 用户 输入一个 paste 的 url 后,可以看到它存储的内容
  • 用户 是匿名的
  • Service 跟踪页面分析
    • 一个月的访问统计
  • Service 删除过期的 pastes
  • Service 需要高可用

超出范畴的用例

  • 用户 可以注册一个账户
    • 用户 通过验证邮箱
  • 用户 可以用注册的账户登录
    • 用户 可以编辑文档
  • 用户 可以设置可见性
  • 用户 可以设置短链接

约束和假设

状态假设

  • 访问流量不是均匀分布的
  • 打开一个短链接应该是很快的
  • pastes 只能是文本
  • 页面访问分析数据可以不用实时
  • 一千万的用户量
  • 每个月一千万的 paste 写入量
  • 每个月一亿的 paste 读取量
  • 读写比例在 10:1

性能估算

  • 每个 paste 的大小
    • 每一个 paste 1 KB
    • shortlink - 7 bytes
    • expiration_length_in_minutes - 4 bytes
    • created_at - 5 bytes
    • paste_path - 255 bytes
    • 总共 = ~1.27 KB
  • 每个月新的 paste 内容在 12.7GB
    • (1.27 * 10000000)KB / 月的 paste
    • 三年内将近 450GB 的新 paste 内容
    • 三年内 3.6 亿短链接
    • 假设大部分都是新的 paste,而不是需要更新已存在的 paste
  • 平均 4paste/s 的写入速度
  • 平均 40paste/s 的读取速度

简单的转换指南:

  • 2.5 百万 req/s
  • 1 req/s = 2.5 百万 req/month
  • 40 req/s = 1 亿 req/month
  • 400 req/s = 10 亿 req/month

步骤二、顶层设计

概述一个包括所有重要的组件的高层次设计

Imgur

步骤三、核心组件设计

深入每一个核心组件的细节

用例:用户输入一段文本,然后得到一个随机生成的链接

我们可以用一个 关系型数据库作为一个大的哈希表,用来把生成的 url 映射到一个包含 paste 文件的文件服务器和路径上。

为了避免托管一个文件服务器,我们可以用一个托管的对象存储,比如 Amazon 的 S3 或者NoSQL 文档类型存储

作为一个大的哈希表的关系型数据库的替代方案,我们可以用NoSQL 键值存储。我们需要讨论选择 SQL 或 NoSQL 之间的权衡。下面的讨论是使用关系型数据库方法。

  • 客户端 发送一个创建 paste 的请求到作为一个反向代理启动的 Web 服务器
  • Web 服务器 转发请求给 写接口 服务器
  • 写接口 服务器执行如下操作:
    • 生成一个唯一的 url
      • 检查这个 url 在 SQL 数据库 里面是否是唯一的
      • 如果这个 url 不是唯一的,生成另外一个 url
      • 如果我们支持自定义 url,我们可以使用用户提供的 url(也需要检查是否重复)
    • 把生成的 url 存储到 SQL 数据库pastes 表里面
    • 存储 paste 的内容数据到 对象存储 里面
    • 返回生成的 url

向面试官阐明你需要写多少代码

pastes 表可以有如下结构:

1
2
3
4
5
shortlink char(7) NOT NULL
expiration_length_in_minutes int NOT NULL
created_at datetime NOT NULL
paste_path varchar(255) NOT NULL
PRIMARY KEY(shortlink)

我们将在 shortlink 字段和 created_at 字段上创建一个数据库索引,用来提高查询的速度(避免因为扫描全表导致的长时间查询)并将数据保存在内存中,从内存里面顺序读取 1MB 的数据需要大概 250 微秒,而从 SSD 上读取则需要花费 4 倍的时间,从硬盘上则需要花费 80 倍的时间。 1

为了生成唯一的 url,我们可以:

  • 使用 MD5 来哈希用户的 IP 地址 + 时间戳
    • MD5 是一个普遍用来生成一个 128-bit 长度的哈希值的一种哈希方法
    • MD5 是一致分布的
    • 或者我们也可以用 MD5 哈希一个随机生成的数据
  • Base 62 编码 MD5 哈希值
    • 对于 urls,使用 Base 62 编码 [a-zA-Z0-9] 是比较合适的
    • 对于每一个原始输入只会有一个 hash 结果,Base 62 是确定的(不涉及随机性)
    • Base 64 是另外一个流行的编码方案,但是对于 urls,会因为额外的 +- 字符串而产生一些问题
    • 以下 Base 62 伪代码 执行的时间复杂度是 O(k),k 是数字的数量 = 7:
1
2
3
4
5
6
7
def base_encode(num, base=62):
digits = []
while num > 0
remainder = modulo(num, base)
digits.push(remainder)
num = divide(num, base)
digits = digits.reverse
  • 取输出的前 7 个字符,结果会有 62^7 个可能的值,应该足以满足在 3 年内处理 3.6 亿个短链接的约束:
1
url = base_encode(md5(ip_address+timestamp))[:URL_LENGTH]

我们将会用一个公开的 REST 风格接口

1
$ curl -X POST --data '{"expiration_length_in_minutes":"60", \"paste_contents":"Hello World!"}' https://pastebin.com/api/v1/paste

Response:

1
2
3
{
"shortlink": "foobar"
}

用于内部通信,我们可以用 RPC

用例:用户输入一个 paste 的 url 后可以看到它存储的内容

  • 客户端 发送一个获取 paste 请求到 Web Server
  • Web Server 转发请求给 读取接口 服务器
  • 读取接口 服务器执行如下操作:
    • SQL 数据库 检查这个生成的 url
      • 如果这个 url 在 SQL 数据库 里面,则从 对象存储 获取这个 paste 的内容
      • 否则,返回一个错误页面给用户

REST API:

1
curl https://pastebin.com/api/v1/paste?shortlink=foobar

Response:

1
2
3
4
5
{
"paste_contents": "Hello World",
"created_at": "YYYY-MM-DD HH:MM:SS",
"expiration_length_in_minutes": "60"
}

用例: 服务跟踪分析页面

因为实时分析不是必须的,所以我们可以简单的 MapReduce Web Server 的日志,用来生成点击次数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class HitCounts(MRJob):

def extract_url(self, line):
"""Extract the generated url from the log line."""
...

def extract_year_month(self, line):
"""Return the year and month portions of the timestamp."""
...

def mapper(self, _, line):
"""Parse each log line, extract and transform relevant lines.

Emit key value pairs of the form:

(2016-01, url0), 1
(2016-01, url0), 1
(2016-01, url1), 1
"""
url = self.extract_url(line)
period = self.extract_year_month(line)
yield (period, url), 1

def reducer(self, key, values):
"""Sum values for each key.

(2016-01, url0), 2
(2016-01, url1), 1
"""
yield key, sum(values)

用例: 服务删除过期的 pastes

为了删除过期的 pastes,我们可以直接搜索 SQL 数据库 中所有的过期时间比当前时间更早的记录,
所有过期的记录将从这张表里面删除(或者将其标记为过期)。

步骤四、扩展设计

给定约束条件,识别和解决瓶颈。

Imgur

重要提示: 不要简单的从最初的设计直接跳到最终的设计

说明您将迭代地执行这样的操作:1)Benchmark/Load 测试,2)Profile 出瓶颈,3)在评估替代方案和权衡时解决瓶颈,4)重复前面,可以参考在 AWS 上设计一个可以支持百万用户的系统这个用来解决如何迭代地扩展初始设计的例子。

重要的是讨论在初始设计中可能遇到的瓶颈,以及如何解决每个瓶颈。比如,在多个 Web 服务器 上添加 负载平衡器 可以解决哪些问题? CDN 解决哪些问题?Master-Slave Replicas 解决哪些问题? 替代方案是什么和怎么对每一个替代方案进行权衡比较?

我们将介绍一些组件来完成设计,并解决可伸缩性问题。内部的负载平衡器并不能减少杂乱。

为了避免重复的讨论, 参考以下系统设计主题获取主要讨论要点、权衡和替代方案:

分析存储数据库 可以用比如 Amazon Redshift 或者 Google BigQuery 这样的数据仓库解决方案。

一个像 Amazon S3 这样的 对象存储,可以轻松处理每月 12.7 GB 的新内容约束。

要处理 平均 每秒 40 读请求(峰值更高),其中热点内容的流量应该由 内存缓存 处理,而不是数据库。内存缓存 对于处理分布不均匀的流量和流量峰值也很有用。只要副本没有陷入复制写的泥潭,SQL Read Replicas 应该能够处理缓存丢失。

对于单个 SQL Write Master-Slave平均 每秒 4paste 写入 (峰值更高) 应该是可以做到的。否则,我们需要使用额外的 SQL 扩展模式:

我们还应该考虑将一些数据移动到 NoSQL 数据库

额外的话题

是否更深入探讨额外主题,取决于问题的范围和面试剩余的时间。

NoSQL

缓存

异步和微服务

通信

安全

参考安全

延迟数字

每个程序员都应该知道的延迟数

持续进行

  • 继续对系统进行基准测试和监控,以在瓶颈出现时解决它们
  • 扩展是一个迭代的过程

《极客时间教程 - 架构实战案例解析》笔记

架构的本质:如何打造一个有序的系统?

架构的本质:通过合理的内部编排,保证系统高度有序,能够不断扩展,满足业务和技术的变化

  • 首先,架构的出发点是业务和技术在不断复杂化,引起系统混乱,需要通过架构来保证有序。

  • 其次,架构实现从无序到有序,是通过合理的内部编排实现的,基本的手段,就是“分”与“合”,先把系统打散,然后将它们重新组合,形成更合理的关系。

    • “分”就是把系统拆分为各个子系统、模块、组件
    • “合”就是基于业务流程和技术手段,把各个组件有机整合在一起

架构的分类

  • 业务架构
  • 应用架构
  • 技术架构

什么是好架构?

  • 一个好的架构设计既要满足业务的可扩展、可复用;
  • 也要满足系统的高可用、高性能和可伸缩,并尽量采用低成本的方式落地。

架构师的自我修养

  • 优秀的程序员
  • 沟通交流(感性)
  • 权衡取舍(理性)
  • 多领域知识(技术的广度)
  • 技术前瞻性(技术的深度)
  • 看透问题本质(思维的深度)
  • 抽象思维(思维的高度)

业务架构:作为开发,你真的了解业务吗?

从架构角度看,业务架构是源头,然后才是技术架构。

业务架构师和产品经理有什么区别?

  • 产品经理定义了系统的外观
    • 告诉用户,系统长什么样子
    • 告诉开发,要实现什么功能
  • 架构师将业务抽象为结构化的模块体系
    • 把业务流程拆分,按照业务域的维度来划分系统模块。
    • 并定义这些模块之间的关系,最终形成一个高度结构化的模块体系。

架构目标之业务的可扩展

业务的主题是变化和创新,系统的主题是稳定和可靠

架构目标之业务的可复用

业务架构设计如何实现业务的可复用呢

首先,模块的职责定位要非常清晰。对于模块来说,在定位范围内的职责要全部涵盖到,而不在这个范围的职责全部不要。

其次,模块的数据模型和接口设计要保证通用。架构师需要归纳业务场景,通过抽象提炼,形成通用化的设计,以此来满足多个类似场景的需求。

最后,实现模块的高复用,还需要做好业务的层次划分。我们知道,越是底层的业务,它就相对更固定。举个例子,同样是订单业务域,对于底层订单的增删改查功能,不同类型的订单都是一样的,但对于上层的订单生命周期管理,外卖订单和堂食订单可能就不一样。

可扩展架构:如何打造一个善变的柔性系统?

系统的构成:模块 + 关系

模块是系统的基本组成部分,它泛指子系统、应用、服务或功能模块。关系指模块之间的依赖关系。

模块的要求:

  • 定位明确,概念完整
  • 自成体系,粒度适中

依赖关系的要求:

  • 最好是单向的
  • 最好是层次化结构

模块的业务逻辑尽量围绕自身内部数据进行处理,对外部依赖越小,模块的封装性越好,稳定性也越强,不会随着外部模块的调整而调整。

业务架构扩展性的本质是:通过构建合理的模块体系,有效地控制系统复杂度,最小化业务变化引起的系统调整。

那如何打造一个合理的模块体系呢?具体的架构手段就是按照业务对系统进行拆分和整合:
通过拆分,实现模块划分;通过整合,优化模块依赖关系。

通过模块通用化,模块的数量减少了,模块的定位更清晰,概念更完整,职责更聚焦。在实
践中,当不同业务线对某个功能需求比较类似时,我们经常会使用这个手段。

通过拆分,实现模块划分;通过整合,优化模块依赖关系。

一般做业务架构时,我们先考虑垂直拆分,从大方向上,把不同业务给区分清楚,然后再针对具体业务,按照业务处理流程进行水平拆分

业务平台化是模块依赖关系层次化的一个特例,只是它偏向于基础能力,在实践中,当业务
线很多,业务规则很复杂时,我们经常把底层业务能力抽取出来,进行平台化处理。

可扩展架构案例(一):电商平台架构是如何演变的?

电商平台架构发展的大致过程:

单体架构

在单体架构中,只有一个应用,所有代码跑在一个进程,所有的表放在一个 DB 里。

单体应用内部一般采用分层结构,从上到下,一般分为表示层、业务层、数据访问层、DB 层。表示层负责用户体验,业务层负责业务逻辑,数据访问层负责 DB 的数据存取。

分布式架构

分布式架构,简单来说就是系统由多个独立的应用组成,它们互相协作,成为一个整体。

分布式架构包括了多个应用,每个应用分别负责不同的业务线,当一个应用需要另一个应用的功能时,会通过 API 接口进行调用。在分布式架构中,API 接口属于应用的一部分,它和表示层共享底层的业务逻辑。

分布式架构适用于业务相关性低、耦合少的业务系统。

SOA 架构

SOA 架构(Service Oriented Architecture)是一种面向服务的架构,它的发展经历了两个阶段:传统的 SOA 架构,它解决的是企业内部大量异构系统集成的问题;新的 SOA 架构,它解决的是系统重复建设的问题。

在 SOA 架构中,每个服务都对应一个现有的系统,所有这些服务都部署在一个中心化的平台上,我们称之为企业服务总线 ESB(Enterprise Service Bus),ESB 负责管理所有调用过程的技术复杂性,包括服务的注册和路由、各种通信协议的支持等等。

微服务架构

微服务强调围绕业务,进行清晰的业务和数据边界划分,并通过良好定义的接口输出业务能力,这和 SOA 架构里的服务有点类似。但两者不同的地方在于,微服务是去中心化的,不需要 SOA 架构中 ESB 的集中管理方式。

一方面,微服务强调所谓的哑管道,即客户端可以通过 HTTP 等简单的技术手段,访问微服务,避免重的通信协议和数据编码支持。另一方面,微服务强调智能终端,所有的业务逻辑包含在微服务内部,不需要额外的中间层提供业务规则处理。

可扩展架构案例(二):App 服务端架构是如何升级的?

V1.0 架构

问题:

  • 移动服务端对 Jar 包的紧密依赖
  • 移动团队的职责过分复杂
  • 团队并行开发困难

V2.0 架构

问题:

  • 移动端和 PC 端互相干扰
  • 重复造轮子
  • 稳定性较差

V3.0 架构

首先,我们对每个业务线的服务端进行拆分,让 App 接口和 PC 端接口各自在物理上独立,但它们共享核心的业务逻辑。

移动网关的内部实现

  • 通用层
    • 首先是通用层,它负责所有系统级功能的处理,比如通讯协议适配、安全、监控、日志等等,这些功能统一由网关的通用层进行预处理,避免了各个业务线的重复开发。
    • 在具体实现时,每个通用功能的处理逻辑都会封装成一个拦截器,这些拦截器遵循统一的接口定义,并且拦截器都是可配置的。当有外部请求过来,网关会依次调用这些拦截器,完成各个系统级功能的处理。
  • 接口路由层
    • 移动端请求经过通用层的预处理之后,将会进一步分发给后端的业务适配器进行处理。
    • 在配置文件里,对接口请求的 URL 和业务适配器进行映射,接口路由层的分发逻辑就是根据请求中的 URL,在配置文件里找到对应的适配器,然后把请求交给适配器进行后续的处理。
  • 服务适配层
    • 适配器首先用来解决内外部接口的适配,除此之外,适配器还可以根据需要,对多个内部服
      务做业务聚合,这样可以对 App 前端提供粗粒度的接口服务,减少远程网络的调用次数。

可扩展架构案例(三):你真的需要一个中台吗?

前台:面向 C 端的应用。前台对外

后台:企业内部系统。后台对内

中台:通过实现基础业务的平台化,实现了企业级业务能力的快速复用

中台的适用性

第一种是独立地建设新业务线,这样,各个业务线并列,系统整体上是一个“川”字型的结构

第二种做法是,把各业务线中相同的核心逻辑抽取出来,通过抽象设计,实现通用化,共同服务于所有业务线的需求,系统结构整体上是一个“山”字型。这样,我们就能一处建设,多处复用,一处修改,多处变化,从而实现最大程度的复用。

何时从“川”字型转为“山”字形呢?

  • 一方面,这和公司业务线的数量有关,业务线越多,意味着重复建设的成本会更大,当我们开始上第 3 条业务线时,就应该要考虑转到“山”字形了。
  • 另一方面,也和各个业务线的相似度有关,相似度越高,意味着业务线之间有更多类似的逻辑,更适合“山”字形。

中台实现了通用基础业务的平台化。从变化速度来看,企业基础的业务是相对固定的,而具体上层业务场景是相对多变的;从数量来看,基础业务数量是有限的,而具体业务场景是无限的。因此,有了完善的中台,我们就可以通过有限而比较固定的基础业务,来满足无限而快速变化的上层业务场景了。

从业务角度来看,中台收敛了业务场景,统一了业务规则;从系统角度看,中台相当于操作系统,对外提供标准接口,屏蔽了底层系统的复杂性;从数据角度看,中台收敛了数据,比如使用同一套订单数据模型,让所有渠道的订单使用相同的订单模型,所有订单数据落到同一个订单库。

中台通过实现基础业务的平台化,实现了企业级业务能力的快速复用。

松散的微服务 -> 共享服务体系 -> 中台

传统企业中台架构设计

中台代表了企业核心的业务能力,它自成体系,能够为 C 端的互联网场景提供通用的能力,并通过各种插件和后台打通。

对于互联网企业来说,有大量微服务做基础,往中台转是改良,目的是更好地衔接前台和后台,实现业务的快速创新;
对于传统企业来说,内部有大量的遗留系统,落地中台是革命,目的是盘活老系统,全面实现企业的数字化转型。

可复用架构:如何实现高层次的复用?

从复用的程度来看,从高到低,我们可以依次划分为产品复用 > 业务流程复用 > 业务实体复用 > 组件复用 > 代码复用。

技术复用:代码级复用和技术组件复用都属于工具层面,它们的好处是在很多地方都可以用,但和业务场景隔得有点远,不直接对应业务功能,因此复用的价值相对比较低。

业务复用

  • 业务实体复用针对细分的业务领域
  • 业务流程的复用针对的是业务场景
  • 最高层次的复用是对整个系统的复用

可复用架构案例(一):如何设计一个基础服务?

对于落地一个共享服务来说,服务边界的划分和功能的抽象设计是核心。

可复用架构案例(二):如何对现有系统做微服务改造?

圈表:圈表就是用来确定库存微服务具体包含哪些表,也就是确定服务的数据模型。

收集 SQL:收集所有业务系统访问这些表的 SQL 语句,包括它的业务场景说明、访问频率等等。库存微服务后续就针对这些 SQL 进行封装,提供相应的接口给业务系统使用。

拆分 SQL:有些 SQL 不仅仅访问圈定的这几张库存表,还会和产品库中的其他表进行关联。

可复用架构案例(三):中台是如何炼成的?

  1. 业务上有什么重大变化,导致当前系统的弊端已经很明显,不能适应业务发展了呢?
  2. 架构改造时,如何在业务、系统、资源三者之间做好平衡,对系统进行分步式的改造呢?

技术架构:作为开发,你真的了解系统吗?

技术架构的职责,首先是负责系统所有组件的技术选型,然后确保这些组件可以正常运行。

业务架构解决的是系统功能性问题

技术架构解决的是系统非功能性问题

技术架构目标

  • 高可用
  • 高性能
  • 伸缩性
  • 安全性

高可用架构:如何让你的系统不掉链子?

故障分类

  • 资源不可用,包括网络和服务器出故障,网络出故障表明节点连接不上,服务器出故障表明该节点本身不能正常工作。
  • 资源不足,常规的流量进来,节点能正常工作,但在高并发的情况下,节点无法正常工作,对外表现为响应超时。
  • 节点的功能有问题,这个主要体现在我们开发的代码上,比如它的内部业务逻辑有问题,或者是接口不兼容导致客户端调用出了问题;另外有些不够成熟的中间件,有时也会有功能性问题。

高可用策略和架构原则

事前,尽量避免问题的发生;始终,要考虑转移故障,降低故障影响,快速恢复系统;事后,要对故障进行复盘,考虑技术、流程上的完善措施。

高可用架构案例(一):如何实现 O2O 平台日订单 500 万?

高可用架构案例(二):如何第一时间知道系统哪里有问题?

高可用架构案例(三):如何打造一体化的监控系统?

高性能和可伸缩架构:业务增长,能不能加台机器就搞定?

  • 加快单个请求处理
    • 优化处理路径上每个节点的处理速度
    • 并行处理单个请求
  • 同时处理多个请求:负载均衡
  • 请求处理异步化:MQ

性能提升思路:

  • 可水平拆分和无状态
  • 短事务和柔性事务
  • 缓存
  • 并行计算
  • 异步处理
  • 容器化

高性能架构案例:如何设计一个秒杀系统?

可伸缩架构案例:数据太多,如何无限扩展你的数据库?

案例:电商平台技术架构是如何演变的?

单体架构

SOA 架构

微服务架构

垂直拆分(分库)

水平拆分

多机房部署

服务调用本地化

依赖分级管理

多机房独立部署

从务实的角度,给你架构设计的重点知识和学习路径

参考资料

架构实战案例解析

《数据密集型应用系统设计》笔记一——数据系统基础

第一章:可靠、可扩展与可维护的应用系统

认识数据系统

单一工具难以满足复杂应用系统的需求,因此整体工作被拆解为一系列能被单个工具高效完成的任务,并通过应用代码将它们缝合起来。比如一个缓存、索引、数据库协作的例子: image.png 一个应用被称为数据密集型的,如果数据是其主要挑战(数据量,数据复杂度、数据变化速度)——与之相对的是计算密集型,即处理器速度是其瓶颈。 软件系统中很重要的三个问题:

  1. 可靠性(Reliability):系统面临各种错误(硬件故障、软件故障、人为错误),仍可正常工作。
  2. 可扩展性(Scalability):有合理的办法应对系统的增长(数据量、流量、复杂性)。
  3. 可维护性(Maintainability):许多不同的人在不同的生命周期,都能高效地在系统上工作。

可靠性

可靠性意味着:即时发生了某些错误,系统仍然可以继续正常工作。

可能出错的事情称为错误(fault)或故障,系统可应对错误则称为容错(fault tolerant)或者弹性(resilient)。

故障与失效(failure)不完全一致。故障通常被定义为组件偏离其正常规格,而失效意味着系统作为一个整体,停止对外提供服务。

常见的故障分类:

  • 硬件故障
    • 故障场景:硬盘崩溃、内存故障、停电、断网等。
    • 应对策略:添加冗余硬件以备用;软件容错(如:负载均衡)。
  • 软件故障
    • 故障场景:各种难以预料的 Bug。
    • 应对策略:仔细考虑细节;全面测试;监控、告警;系统/数据隔离机制;自动化部署、回滚机制等。
  • 人为失误
    • 故障场景:操作不当、配置错误等。
    • 应对策略:快速恢复机制;监控、告警等。

可扩展性

可扩展性(Scalability)是用来描述系统应对负载增长能力的术语。

描述负载

负载可以用称为负载参数的若干数字来描述。参数的最佳选择取决于系统的体系结构。它可能是 QPS、数据库中写入的比例、日活用户量、缓存命中率等。

推特发送推文的设计变迁:

推文放在全局推文集合中,查询的时候做 join

image.png

推文插入到每个关注者的时间线中,「扇出」比较大,当有千万粉丝的大 V 发推压力大

image.png

推特从方案一变成了方案二,然后变成了两者结合的方式

描述性能

负责增加将会发生什么:

  1. 负载增加,但系统资源保持不变时,系统性能将受到什么影响?
  2. 负载增加,如果希望性能保持不变时,需要增加多少系统资源?

批处理系统,通常关心吞吐量(throughput);在线系统,通常更关心响应时间(response time)。

度量场景的响应时间,平均响应时间并不是一个合适的指标,因为它无法告诉有多少用户实际经历了多少延迟。最好使用百分位数,比如中位数(P50)、P95、P99、P999 等标识。

image.png

测量客户端的响应时间非常重要(而不是服务端),比如会出现头部阻塞、网络延迟等。

实践中的百分位点,可以用一个滑动的时间窗口(比如 10 分钟)进行统计。可以对列表进行排序,效率低的话,考虑一下正向衰减,t-digest 等近似计算方法。

image.png

响应时间:中位数指标比平均响应时间更适合描述等待时间。

如何应对负载:垂直扩展(升级硬件)和水平扩展(集群、分布式)

应对负载的方法

  • 垂直扩展:升级硬件
  • 水平扩展:将负载分布到多台小机器上
  • 弹性设计:自动检测负载增加,然后自动添加计算资源
  • 无状态服务可以组成集群进行扩展;有状态服务从单点到分布式,复杂性会大大增加,因此,应该尽量将数据库放在单节点上。

可维护性

三个设计原则:

  • 可运维性:运维更轻松。应对:监控、链路追踪、CI/CD、规范流程等。
  • 简单性:简化复杂度。应对:良好的抽象。
  • 可演化性:易于改变。应对:DDD、TDD、重构、敏捷。

第二章:数据模型与查询语言

关系模型与文档模型

关系模型 - 数据被组织成关系(SQL 中称作),其中每个关系是元组(SQL 中称作) 的无序集合。

NoSql - 不仅是 SQL(Not Only SQL)

相比于关系型数据库,为什么用 NoSql?

  • 需要更好的扩展性,以应对非常大的数据集或高并发。
  • 关系模型不能很好地支持一些特殊的查询。
  • 关系模型有很多限制,不够灵活。

当前以及未来很长一段时间,关系型数据库和 NoSql 并存的混合持久化是一种常态。

复杂的应用程序可能会有更多的中间层,每层都通过提供一个简洁的数据模型来隐藏下层的复杂性。

如果数据大多是一对多关系(树结构数据)或者记录之间没有关系,那么文档模型是最合适的。

关系模型能够处理简单的多对多关系,但是随着数据之间的关联越来越复杂,将数据建模转化为图模型会更加自然。

对象关系不匹配

使用面向对象语言,需要一个转换层,才能转成 SQL 数据模型。模型之间的脱离有时被称为阻抗失谐。

Hibernate 这样的 对象关系映射(ORM) 框架则减少这个转换层所需的样板代码量,但是它们不能完全隐藏这两个模型之间的差异。

对于一份简历而言,关系型模型描述一对多的关系需要多张表。

image.png 对于简历这样的数据结构,主要是一个自包含的文档,用 JSON 表示非常合适。JSON 相比于多表模式,有更好的局部性,可以一次查询出一个用户的所有信息。JSON 其实是树形层级结构。image.png

多对一和多对多的关系

使用 ID 的好处是,因为它对人类没有任何直接意义,所以永远不需要直接改变:即使 ID 标识的信息发生了变化,它也可以保持不变。

文档模型不适合表达多对一的关系。对于关系数据库,由于支持联结操作,可以更方便地通过 ID 来引用其他表的行。而在文档数据库中,一对多的树状结构不需要联结,即使支持联结通常也比较弱。

如果数据库本身不支持联结,则必须通过对数据库进行多次查询来模拟联结。

考虑以下可能对简历进行的修改或补充:

  • 组织和学校作为实体:组织、学校有各自的主页。
  • 推荐:用户可以推荐其他用户在自己的简历上。

image.png

文档数据库是否在重演历史?

20 世纪 70 年代,最受欢迎的是层次模型(hierarchical model),它与文档数据库使用的 JSON 模型有很多相似之处。它将所有数据表示为嵌套在记录中的记录树。层次模型能很好地支持一对多的关系,但是很难支持多对多的关系,而且不支持联结。

为解决层次模型的局限性而提出的方案:

  • 关系模型(relational model) - 后来,演变成了 SQL,并被广泛接受
  • 网络模型(network model) - 最初很受关注,但最终被淡忘

image.png

网络模型

每个记录可能有多个父节点。

网络模型中,记录之间的链接不是外键,而更像编程语言中的指针(会存储在磁盘上)。访问记录的唯一方法是选择一条始于根记录的路径,并沿着相关链接一次访问,这条链接链条也被称为访问路径(access path)

最简单的情况下,访问路径类似遍历链表:从链表头开始,每次查看一条记录,直到找到所需的记录。但在多对多关系的情况中,存在多条不同的路径可以通向相同的记录,网络模型的程序员必须跟踪这些不同的访问路径。

缺点:查询和更新数据库非常麻烦。

关系模型

关系模型定义了所有数据的格式:关系(表) 只是 元组(行) 的集合,仅此而已。

在关系数据库中,查询优化器自动决定以何种顺序执行查询,以及使用哪些索引。

文档数据库的比较

文档数据库是某种方式的层次模型:即在其负记录中保存了嵌套记录,而不是存储在单独的表中。

但是,在表示多对一和多对多的关系时,关系数据库和文档数据库并没有根本的不同:在这两种情况下,相关项目都由唯一的标识符引用,该标识符在关系模型中被称为外键,在文档模型中被称为文档引用。标识符可以查询时通过联结操作或相关后续查询来解析。

关系数据库与文档数据库现状

支持文档数据模型的主要论据是模式灵活性,由于局部性而带来较好的性能。关系模型则强在联结操作、多对一和多对多关系更简洁的表达上。

哪种数据模型的应用代码更简单

文档模型:

  • 优点:
    • 如果应用程序中的数据具有类似文档的结构(即一对多关系树,通常一次性加载整个树),那么使用文档模型更为合适。而关系模型则倾向于数据分解,把文档结构分解为多个表。
  • 缺点:
    • 不能直接引用文档中的嵌套的项目,而是需要说“用户 251 的位置列表中的第二项”(很像分层模型中的访问路径)。但是,只要文件嵌套不太深,这通常不是问题。
    • 文档数据库对联结的支持不足。这是否是问题取决于应用,如果应用程序使用多对多关系,那么文档模型就没不合适了。

对于高度关联的数据,文档模型不太适合,关系模型更适合。

文档模型中的模式灵活性

文档模型是「读时模式」

  • 文档数据库有时称为无模式(schemaless),但这具有误导性,因为读取数据的代码通常假定某种结构——即存在隐式模式,但不由数据库强制执行。
  • 一个更精确的术语是读时模式(schema-on-read)(数据的结构是隐含的,只有在数据被读取时才被解释),相应的是写时模式(schema-on-write)(传统的关系数据库方法中,模式明确,且数据库确保数据写入时都必须遵循)。
  • 读时模式类似于编程语言中的动态(运行时)类型检查,而写时模式类似于静态(编译时)类型检查。

模式变更

  • 读时模式变更字段很容易,只用改应用代码
  • 写时模式变更字段速度很慢,而且要求停运。它的这种坏名誉并不是完全应得的:大多数关系数据库系统可在几毫秒内执行 ALTER TABLE 语句。MySQL 是一个值得注意的例外,它执行 ALTER TABLE 时会复制整个表,这可能意味着在更改一个大型表时会花费几分钟甚至几个小时的停机时间,尽管存在各种工具来解决这个限制。
查询的数据局部性

文档通常存储为编码为 JSON、XML 或其二进制变体(如 MongoDB 的 BSON)的连续字符串。

读文档:

  • 如果应用需要频繁访问整个文档,则存储局部性具有性能优势。
  • 局部性优势仅适用于需要同时访问文档大部分内容的场景。

写文档:

  • 更新文档时,通常需要重写整个文档。
  • 通常建议文档应该尽量小且避免写入时增加文档大小。
文档数据库与关系数据库的融合
  • MySQL 等逐步增加了对 JSON 和 XML 的支持
  • 融合关系模型与文档模型是未来数据库发展的一条很好的途径。

数据查询语言

  • 关系模型包含了一种查询数据的新方法:SQL 是一种 声明式 查询语言,而 IMS 和 CODASYL 使用 命令式 代码来查询数据库。

  • 命令式语言告诉计算机以特定顺序执行某些操作,比如常见的编程语言。

  • 声明式查询语言只需指定所需的数据模式,结果需要满足哪些条件,以及如何转换数据(例如,排序,分组和集合) ,而不需指明如何实现这一目标

Web 上的声明式查询(略)

MapReduce 查询

MapReduce 是一种编程模型,用于在许多机器上批量处理海量数据。一些 NoSQL 支持有限的 MapReduce 方式在大量文档上执行只读查询。

图数据模型(略)

本章小结

历史上,数据最初被表示为一棵大树(层次模型),但是这不利于表示多对多的关系,所以发明了关系模型来解决这个问题。 最近,开发人员发现一些应用程序也不适合采用关系模型。新的非关系型“NoSQL”数据存储在两个主要方向上存在分歧:

  • 文档数据库的应用场景是:数据来自于自包含文档,且文档之间的关联很少。
  • 图数据库则的应用场景是:所有数据都可能会相互关联。

文档模型、关系模型和图模型,都应用广泛。不同模型之间可以相互模拟,但是处理起来比较笨拙。

文档数据库和图数据库有一个共同点,那就是它们通常不会对存储的数据强加某个模式,这样比较灵活。

第三章:存储与检索

从最基本的层面看,数据库只需做两件事情:存储和检索。

数据库核心:数据结构

为了高效地查找数据库中特定键的值, 需要新的数据结构: 索引。

存储系统的设计权衡:适当的索引可以加速读取查询,但每个索引都会减慢写速度。数据库通常不会对所有内容进行索引。

索引类型:

  • 哈希索引
  • B+ 树
  • LSM 树
  • 等等

扩展阅读:检索技术核心 20 讲

事务处理与分析处理

列式存储

如果表中有数以万亿行、PB 大小的数据,则适合用于存储在列式存储中。

第四章:数据编码与演化

本章节主要介绍各种序列化、反序列化方式。略

数据编码格式

数据流模式

向前和向后的兼容对于可演化性来说非常重要。

基于数据库的数据流

在不同的时间写入不同的值

数据库通常支持在不同的时间写入不同的值。

在集群中部署新版本是一个逐一的过程,必然存在这样的时间段:集群中部分是新机器,部分是老机器。

当旧版本的应用视图更新新版本的应用所写入的数据时,可能会丢失数据。

image.png

归档数据

生成数据库快照时,数据转储通常使用最新的模式进行编码。

基于服务的数据流:REST 和 RPC

  • 最常见的网络通信方式:C/S 架构(客户端+服务端)。
  • Web 服务:收、发 GET 和 POST 请求。
  • 将大型应用分而治之:微服务架构。
  • 微服务架构的一个关键设计目标:服务可以独立部署和演化。
Web 服务
  • 当 HTTP 被用作与服务通信的底层协议时,它被称为 Web 服务
  • 有两种流行的 Web 服务方法:REST 和 SOAP。

REST 不是一种协议,而是一个基于 HTTP 原则的设计理念。它强调简单的数据格式,使用 URL 来标识资源,并使用 HTTP 功能进行缓存控制,身份验证和内容类型协商。与 SOAP 相比,REST 已经越来越受欢迎,至少在跨组织服务集成的背景下,并经常与微服务相关。根据 REST 原则设计的 API 称为 RESTful。

SOAP 是一种基于 XML 的协议,用于发送网络 API 请求。虽然,它最常用于 HTTP,但其目的是独立于 HTTP,并避免使用大多数 HTTP 功能。SOAP Web 服务的 API 使用 WSDL 语言来描述。 WSDL 支持代码生成,客户端可以使用本地类和方法调用(编码为 XML 消息并由框架再次解码)访问远程服务。尽管 SOAP 及其各种扩展表面上是标准化的,但是不同厂商的实现之间的互操作性往往会造成问题。

远程过程调用(RPC)的问题

RPC 模型试图向远程网络服务发出请求,看起来与在同一进程中调用编程语言中的函数或方法相同(这种抽象称为位置透明)。

RPC 的缺陷:

  • 本地函数调用是可预测的,并且成功或失败仅取决于控制的参数。而网络请求是不可预知的。
  • 本地函数调用要么返回结果,要么抛出异常,或者永远不返回(因为进入无限循环或进程崩溃)。网络请求有另一个可能的结果:由于超时,它可能会没有返回结果。这种情况下,无法得知发生了什么。
  • 如果重试失败的网络请求,可能会发生请求实际上已经完成,只有响应丢失的情况。在这种情况下,重试将导致该操作被执行多次,除非在协议中建立重复数据消除( 幂等(idempotence))机制。本地函数调用没有这个问题。
  • 每次调用本地功能时,通常需要大致相同的时间来执行。网络请求慢得多,不可预知。
  • 调用本地函数时,可以高效地将引用(指针)传递给本地内存中的对象。当发出网络请求时,所有这些参数都需要被编码成可以通过网络发送的字节序列。如果参数是像数字或字符串这样的基本类型倒是没关系,但是对于较大的对象很快就会变成问题。
  • 客户端和服务端可以用不同的编程语言实现。所以,RPC 框架必须将数据类型从一种语言转换成另一种语言。

RPC 比 REST 性能好。但是,REST 更加方便,不限定特定的语言,有更好的通用性。因此,REST 是公共 API 的主流;RPC 框架则侧重于同一组织内多个服务间的请求,且通常在同一数据中心。

基于消息传递的数据流

消息代理

通常,消息代理的使用方式如下:

生产者向指定的队列或主题发消息;消息代理确保消息被传递给队列或主题的一个或多个消费者或订阅者。同一主题上,可以有多个生产者和多个消费者。

分布式 Actor 框架

Actor 模型是用于单个进程中并发的编程模型。每个 Actor 通常代表一个客户端或实体,它可能具有某些本地状态,并且它通过发送和接受异步消息与其他 Actor 通信。

分布式的 Actor 框架实质上时将消息代理和 Actor 编程模型集成到单个框架中。

三种流行的分布式 Actor 框架:

  • Akka 使用 Java 的内置序列化,它不提供向前或向后兼容性。但是,可以用类似 Protocol Buffer 替代;
  • Orleans 不支持滚动升级部署的自定义数据编码格式;
  • Erlang OTP,很难对记录模式进行更改。

小结

许多服务需要支持滚动升级:向前、向后兼容性。

我们讨论了几种数据编码格式及其兼容性属性:

  • 编程语言特定的编码仅限于单一编程语言,往往无法提供前向和后向兼容性。
  • JSON,XML 和 CSV 等文本格式非常普遍,其兼容性取决于您如何使用它们。它们有可选的模式语言,这有时是有用的,有时却是一个障碍。这些格式对某些数据类型的支持有些模糊,必须小心数字和二进制字符串等问题。
  • 像 Thrift,Protocol Buffers 和 Avro 这样的二进制模式驱动格式,支持使用清晰定义的前向和后向兼容性语义进行紧凑,高效的编码。这些模式对于静态类型语言中的文档和非常有用。但是,他们有一个缺点,就是在数据可读之前需要对数据进行解码。

我们还讨论了数据流的几种模式,说明了数据编码重要性的不同场景:

  • 数据库,写入数据库的进程对数据进行编码,并从数据库读取进程对其进行解码。
  • RPC 和 REST API,客户端对请求进行编码,服务器对请求进行解码并对响应进行编码,客户端最终对响应进行解码。
  • 异步消息传递(使用消息代理或 Actor),节点之间通过互发消息进行通信,消息由发送者编码并由接收者解码。

结论:前向兼容性和滚动升级在某种程度上是可以实现的。

参考资料

《数据密集型应用系统设计》笔记二

第五章:数据复制

复制主要指通过网络在多台机器上保存相同数据的副本。通过复制,可以达到以下目的:

  • 使数据在地理位置上更接近用户,从而降低访问延迟。如:CDN
  • 当部分组件出现故障,系统依然可以继续工作,从而提高可用性。
  • 扩展至多台机器以同事提供数据访问服务,从而提高读吞吐量。

主流的复制模式:主从复制、多主复制、无主复制。

复制需要考虑的细节:同步复制还是异步复制?如何处理失败的副本(故障转移)?处理策略通常采用可配置项来调整。

主节点与从节点

每个保存数据库完整数据集的节点称之为副本。有了多副本,必然会面临一个问题:如何确保所有副本之间的数据是一致的?

主从复制的工作原理如下:

  1. 指定某一个副本为主副本(或称为主节点) 。当客户写数据库时,必须将写请求首先发送给主副本,主副本首先将新数据写入本地存储。
  2. 其他副本则全部称为从副本(或称为从节点)。主副本把新数据写入本地存储后,然后将数据更改作为复制的日志或更改流发送给所有从副本。每个从副本获得更改日志之后将其应用到本地,且严格保持与主副本相同的写入顺序。
  3. 客户端从数据库中读数据时,可以在主副本或者从副本上执行查询。再次强调,只有主副本才可以接受写请求:从客户端的角度来看,从副本都是只读的。

img

支持主从复制的案例:

  • 关系型数据库:MySql、SQL Server、PostgreSQL 等
  • Nosql:MongoDB、Redis 等
  • 消息队列:Kafka、RabbitMQ 等

同步复制与异步复制

复制的基本流程是,客户将更新请求发送给主节点,主节点接收到请求,接下来将数据更新转发给从节点。最后,由
主节点来通知客户更新完成。

img

通常情况下, 复制速度会非常快,例如多数数据库系统可以在一秒之内完成所有从节点的更新。但是,系统其
实并没有保证一定会在多长时间内完成复制。有些情况下,从节点可能落后主节点几分钟甚至更长时间,例如,由于从节点刚从故障中恢复,或者系统已经接近最大设计上限,或者节点之间的网络出现问题。

  • 同步复制的优点: 一旦向用户确认,从节点可以明确保证完成了与主节点的更新同步,数据已经处于最新版本。万一主节点发生故障,总是可以在从节点继续访问最新数据。
  • 同步复制的缺点:如果同步的从节点无法完成确认(例如由于从节点发生崩愤,或者网络故障,或任何其他原因), 写入就不能视为成功。主节点会阻塞其后所有的写操作,直到同步副本确认完成。

因此,把所有从节点都配置为同步复制有些不切实际。因为这样的话,任何一个同步节点的中断都会导致整个系统更新停滞不前。

实际应用中,很多数据库推荐的模式是:只要有一个从节点或半数以上的从节点同步成功,就视为同步,直接返回结果;剩下的节点都通过异步方式同步。

  • 异步复制的优点:不管从节点上数据多么滞后,主节点总是可以继续响应写请求,系统的吞吐性能更好。
  • 异步复制的缺点:如果主节点发生失败且不可恢复,则所有尚未复制到从节点的写请求都会丢失。

主从复制还经常会被配置为全异步模式。此时如果主节点发生失败且不可恢复,则所有尚未复制到从节点的写请求都会丢失。这意味着即使向客户端确认了写操作, 却无法保证数据的持久化。

配置新的从节点

要做到在不停机、服务不中断的前提下,完成配置新的从节点,主要操作步骤是:

  1. 在某个时间点对主节点的数据副本产生一个一致性快照,这样避免长时间锁定整个数据库。目前大多数数据库都支持此功能,快照也是系统备份所必需的。而在某些情况下,可能需要第三方工具, 如 MySQL 的 innobackupex。
  2. 将此快照拷贝到新的从节点。
  3. 从节点连接到主节点并请求快照点之后所发生的数据更改日志。因为在第一步创建快照时,快照与系统复制日志的某个确定位置相关联,这个位置信息在不同的系统有不同的称呼,如 PostgreSQL 将其称为“ log sequence number” (日志序列号),而 MySQL 将其称为“ binlog coordinates ” 。
  4. 获得日志之后,从节点来应用这些快照点之后所有数据变更,这个过程称之为追赶。接下来,它可以继续处理主节点上新的数据变化。井重复步骤 1 ~步骤 4 。

建立新的从副本具体操作步骤可能因数据库系统而异。

处理节点失效

如何通过主从复制技术来实现系统高可用呢?

从节点失效: 追赶式恢复

从节点的本地磁盘上都保存了副本收到的数据变更日志。如果从节点发生崩溃,然后顺利重启,或者主从节点之间的网络发生暂时中断(闪断),则恢复比较容易,根据副本的复制日志,从节点可以知道在发生故障之前所处理的最后一笔事务,然后连接到主节点,并请求自那笔事务之后中断期间内所有的数据变更。在收到这些数据变更日志之后,将其应用到本地来追赶主节点。之后就和正常情况一样持续接收来自主节点数据流的变化。

主节点失效:节点切换

选择某个从节点将其提升为主节点;客户端也需要更新,这样之后的写请求会发送给新的主节点,然后其他从节点要接受来自新的主节点上的变更数据,这一过程称之为切换。

自动切换的步骤通常如下:

  1. 确认主节点失效。有很多种出错可能性,所以大多数系统都采用了基于超时的机制:节点间频繁地互相发生发送心跳悄息,如果发现某一个节点在一段比较长时间内(例如 30s )没有响应,即认为该节点发生失效。
  2. 选举新的主节点。可以通过选举的方式(超过多数的节点达成共识)来选举新的主节点,或者由之前选定的某控制节点来指定新的主节点。候选节点最好与原主节点的数据差异最小,这样可以最小化数据丢失的风险。让所有节点同意新的主节点是个典型的共识问题。
  3. 重新配置系统使新主节点生效。客户端现在需要将写请求发送给新的主节点。如果原主节点之后重新上线,可能仍然自认为是主节点,而没有意识到其他节点已经达成共识迫使其下台。这时系统要确保原主节点降级为从节点,并认可新的主节点。

上述切换过程依然充满了很多变数:

  • 如果使用了异步复制,且失效之前,新的主节点并未收到原主节点的所有数据;在选举之后,原主节点很快又重新上线并加入到集群,接下来的写操作会发生什么?新的主节点很可能会收到冲突的写请求,这是因为原主节点未意识的角色变化,还会尝试同步其他从节点,但其中的一个现在已经接管成为现任主节点。常见的解决方案是,原主节点上未完成复制的写请求就此丢弃,但这可能会违背数据更新持久化的承诺。
  • 如果在数据库之外有其他系统依赖于数据库的内容并在一起协同使用,丢弃数据的方案就特别危险。例如,在 GitHub 的一个事故中,某个数据并非完全同步的 MySQL 从节点被提升为主副本,数据库使用了自增计数器将主键分配给新创建的行,但是因为新的主节点计数器落后于原主节点( 即二者并非完全同步),它重新使用了已被原主节点分配出去的某些主键,而恰好这些主键已被外部 Redis 所引用,结果出现 MySQL 和 Redis 之间的不一致,最后导致了某些私有数据被错误地泄露给了其他用户。
  • 在某些故障情况下,可能会发生两个节点同时都自认为是主节点。这种情况被称为脑裂,它非常危险:两个主节点都可能接受写请求,并且没有很好解决冲突的办法,最后数据可能会丢失或者破坏。作为一种安全应急方案,有些系统会采取措施来强制关闭其中一个节点。然而,如果设计或者实现考虑不周,可能会出现两个节点都被关闭的情况。
  • 如何设置合适的超时来检测主节点失效呢? 主节点失效后,超时时间设置得越长也意味着总体恢复时间就越长。但如果超时设置太短,可能会导致很多不必要的切换。例如,突发的负载峰值会导致节点的响应时间变长甚至超肘,或者由于网络故障导致延迟增加。如果系统此时已经处于高负载压力或网络已经出现严重拥塞,不必要的切换操作只会使总体情况变得更糟。

复制日志的实现

基于语句的复制

最简单的情况,主节点记录所执行的每个写请求(操作语句)井将该操作语句作为日志发送给从节点。对于关系数据库,这意味着每个 INSERT 、UPDATE 或 DELETE 语句都会转发给从节点,并且每个从节点都会分析井执行这些 SQL 语句,如同它们是来自客户端那样。

听起来很合理也不复杂,但这种复制方式有一些不适用的场景:

  • 任何调用非确定性函数的语句,如 NOW() 获取当前时间,或 RAND() 获取一个随机数等,可能会在不同的副本上产生不同的值。
  • 如果语句中使用了自增列,或者依赖于数据库的现有数据(例如, UPDATE ... WHERE <某些条件>),则所有副本必须按照完全相同的顺序执行,否则可能会带来不同的结果。进而,如果有多个同时并发执行的事务时, 会有很大的限制。
  • 有副作用的语句(例如,触发器、存储过程、用户定义的函数等),可能会在每个副本上产生不同的副作用。

有可能采取一些特殊措施来解决这些问题,例如,主节点可以在记录操作语句时将非确定性函数替换为执行之后的确定的结果,这样所有节点直接使用相同的结果值。但是,这里面存在太多边界条件需要考虑,因此目前通常首选的是其他复制实现方案。

MySQL 5.1 版本之前采用基于操作语句的复制。现在由于逻辑紧凑,依然在用,但是默认情况下,如果语句中存在一些不确定性操作,则 MySQL 会切换到基于行的复制(稍后讨论)。VoltDB 使用基于语句的复制,它通过事务级别的确定性来保证复制的安全。

基于预写日志(WAL)传输

通常每个写操作都是以追加写的方式写入到日志中:

  • 对于日志结构存储引擎,日志是主要的存储方式。日志段在后台压缩井支持垃圾回收。
  • 对于采用覆写磁盘的 BTree 结构,每次修改会预先写入日志,如系统发生崩溃,通过索引更新的方式迅速恢复到此前一致状态。

不管哪种情况,所有对数据库写入的字节序列都被记入日志。因此可以使用完全相同的日志在另一个节点上构建副本:除了将日志写入磁盘之外, 主节点还可以通过网络将其发送给从节点。

PostgreSQL 、Oracle 以及其他系统等支持这种复制方式。其主要缺点是日志描述的数据结果非常底层: 一个 WAL 包含了哪些磁盘块的哪些字节发生改变,诸如此类的细节。这使得复制方案和存储引擎紧密耦合。如果数据库的存储格式从一个版本改为另一个版本,那么系统通常无法支持主从节点上运行不同版本的软件。

基于行的逻辑日志复制

关系数据库的逻辑日志通常是指一系列记录来描述数据表行级别的写请求:

  • 对于行插入,日志包含所有相关列的新值。
  • 对于行删除,日志里有足够的信息来唯一标识已删除的行,通常是靠主键,但如果表上没有定义主键,就需要记录所有列的旧值。
  • 对于行更新,日志包含足够的信息来唯一标识更新的行,以及所有列的新值(或至少包含所有已更新列的新值)。

如果一条事务涉及多行的修改,则会产生多个这样的日志记录,并在后面跟着一条记录,指出该事务已经提交。MySQL 的二进制日志 binlog (当配置为基于行的复制时)使用该方式。

由于逻辑日志与存储引擎逻辑解耦,因此可以更容易地保持向后兼容,从而使主从节点能够运行不同版本的软件甚至是不同的存储引擎。

对于外部应用程序来说,逻辑日志格式也更容易解析。

基于触发器的复制

在某些情况下,我们可能需要更高的灵活性。例如,只想复制数据的一部分,或者想从一种数据库复制到另一种数据库,或者需要订制、管理冲突解决逻辑( 参阅本章后面的“处理写冲突”),则需要将复制控制交给应用程序层。

有一些工具,可以通过读取数据库日志让应用程序获取数据变更。另一种方法则是借助许多关系数据库都支持的功能:触发器和存储过程。

触发器支持注册自己的应用层代码,使得当数据库系统发生数据更改(写事务)时自动执行上述自定义代码。通过触发器技术,可以将数据更改记录到一个单独的表中,然后外部处理逻辑访问该表,实施必要的自定义应用层逻辑,例如将数据更改复制到另一个系统。Oracle 的 Databus 和 Postgres 的 Bucardo 就是这种技术的典型代表。基于触发器的复制通常比其他复制方式开销更高, 也比数据库内置复制更容易出错,或者暴露一些限制。然而,其高度灵活性仍有用武之地。

复制滞后问题

主从复制要求所有写请求都经由主节点,而任何副本只能接受只读查询。对于读操作密集的负载(如 Web ),这是一个不错的选择:创建多个从副本,将读请求分发给这些从副本,从而减轻主节点负载井允许读取请求就近满足——读写分离。

在这种扩展体系下,只需添加更多的从副本,就可以提高读请求的服务吞吐量。但是,这种方法实际上只能用于异步复制,如果试图同步复制所有的从副本,则单个节点故障或网络中断将使整个系统无法写入。而且节点越多,发生故障的概率越高,所以完全同步的配置现实中反而非常不可靠。

不幸的是,如果一个应用正好从一个异步的从节点读取数据,而该副本落后于主节点,则应用可能会读到过期的信息。这会导致数据库中出现明显的不一致:由于并非所有的写入都反映在从副本上,如果同时对主节点和从节点发起相同的查询,可能会得到不同的结果。经过一段时间之后,从节点最终会赶上并与主节点数据保持一致。这种效应也被称为最终一致性

读自己的写

许多应用让用户提交一些数据,接下来查看他们自己所提交的内容。例如客户数据库中的记录,亦或者是讨论主题的评论等。提交新数据须发送到主节点,但是当用户读取数据时,数据可能来自从节点。这对于读密集和偶尔写入的负载是个非常合适的方案。

然而对于异步复制存在这样一个问题,用户在写入不久即查看数据,则新数据可能尚未到达从节点。对用户来讲, 看起来似乎是刚刚提交的数据丢失了。

img

对于这种情况,我们需要强一致性。如何实现呢?有以下方案:

  • 如果用户访问可能会被修改的内容,从主节点读取; 否则,在从节点读取。这背后就要求有一些方法在实际执行查询之前,就已经知道内容是否可能会被修改。例如,社交网络上的用户首页信息通常只能由所有者编辑,而其他人无法编辑。因此,这就形成一个简单的规则: 总是从主节点读取用户自己的首页配置文件,而在从节点读取其他用户的配置文件。
  • 如果应用的大部分内容都可能被所有用户修改,那么上述方法将不太有效,它会导致大部分内容都必须经由主节点,这就丧失了读操作的扩展性。此时需要其他方案来判断是否从主节点读取。例如,跟踪最近更新的时间,如果更新后一分钟之内,则总是在主节点读取;井监控从节点的复制滞后程度,避免从那些滞后时间超过一分钟的从节点读取。
  • 客户端还可以记住最近更新时的时间戳,井附带在读请求中,据此信息,系统可以确保对该用户提供读服务时都应该至少包含了该时间戳的更新。如果不够新,要么交由另一个副本来处理,要么等待直到副本接收到了最近的更新。时间戳可以是逻辑时间戳(例如用来指示写入顺序的日志序列号)或实际系统时钟。
  • 如果副本分布在多数据中心(例如考虑与用户的地理接近,以及高可用性),情况会更复杂些。必须先把请求路由到主节点所在的数据中心(该数据中心可能离用户很远)。

如果同一用户可能会从多个设备访问数据,情况会更加复杂。

  • 记住用户上次更新时间戳的方法实现起来会比较困难,因为在一台设备上运行的代码完全无法知道在其他设备上发生了什么。此时,元数据必须做到全局共享。
  • 如果副本分布在多数据中心,无法保证来自不同设备的连接经过路由之后都到达同一个数据中心。例如,用户的台式计算机使用了家庭宽带连接,而移动设备则使用蜂窝数据网络,不同设备的网络连接线路可能完全不同。如果方案要求必须从主节点读取,则首先需要想办毡确保将来自不同设备的请求路由到同一个数据中心。

单调读

img

假设用户从不同副本进行了多次读取,可能会出现:用户看到了最新内容之后又读到了过期的内容,好像时间被回拨, 此时需要单调读一致性。

单调读一致性可以确保不会发生这种异常。单调读一致性是一个比强一致性弱,但比最终一致性强的保证。当读取数据时,单调读保证,如果某个用户依次进行多次读取,则他绝不会看到回滚现象,即在读取较新值之后又发生读旧值的情况。

实现单调读的一种方式是,确保每个用户总是从固定的同一副本执行读取(而不同的用户可以从不同的副本读取)。

前缀一致读

前缀一致读:对于一系列按照某个顺序发生的写请求,那么读取这些内容时也会按照当时写入的顺序

如果数据库总是以相同的顺序写入,则读取总是看到一致的序列,不会发生这种反常。然而,在许多分布式数据库中,不同的分区独立运行,因此不存在全局写入顺序。这就导致当用户从数据库中读数据时,可能会看到数据库的某部分旧值和另一部分新值。

image.png

一个解决方案是确保任何具有因果顺序关系的写入都交给一个分区来完成,但该方案效率很低。此外,还有一些算法来显示地跟踪事件因果关系(Happend Before)。

复制滞后的解决方案

解决方案是分布式事务。

多主节点复制

主从复制存在一个明显的缺点:系统只有一个主节点,而所有写入都必须经由主节点。主从复制方案就会影响所有的写入操作。典型代表:ZooKeeper。

适用场景

在一个数据中心内部使用多主节点基本没有太大意义,其复杂性已经超过所能带来的好处。

但是,以下场景这种配置则是合理的:

  • 多数据中心
  • 离线客户端操作
  • 协作编辑

多数据中心

为了容忍整个数据中心级别故障或更接近用户,可以把数据库的副本横跨多个数据中心。

在每个数据中心内,采用常规的主从复制;而在数据中心之间,由各数据中心的主节点来负责同其他数据中心的主节点进行数据的交换、更新。

image.png

主从复制和多主复制的差异:

  • 性能:对于主从复制,每个写请求都必须经由广域网传送至主节点所在的数据中心。这会大大增加写入延迟,井基本偏离了采用多数据中心的初衷(即就近访问)。而在多主节点模型中,每个写操作都可以在本地数据中心快速响应,然后采用异步复制方式将变化同步到其他数据中心。因此,对上层应用有效屏蔽了数据中心之间的网络延迟,使得终端用户所体验到的性能更好。
  • 容忍数据中心失效:对于主从复制,如果主节点所在的数据中心发生故障,必须切换至另一个数据中心,将其中的一个从节点被提升为主节点。在多主节点模型中,每个数据中心则可以独立于其他数据中心继续运行,发生故障的数据中心在恢复之后更新到最新状态。
  • 容忍网络问题:数据中心之间的通信通常经由广域网,它往往不如数据中心内的本地网络可靠。对于主从复制模型,由于写请求是同步操作,对数据中心之间的网络性能和稳定性等更加依赖。多主节点模型则通常采用异步复制,可以更好地容忍此类问题,例如临时网络闪断不会妨碍写请求最终成功。

离线客户端操作

  • 多主复制的另一适用场景:应用在断网后仍然需要继续工作。典型代表:各种电子笔记软件。
  • 在这种情况下,每个设备都有一个充当领导者的本地数据库(用来接受写请求),然后在所有设备上采用异步方式同步这些多主节点的副本,同步滞后可能是几小时或数天。
  • 从架构层面来看,每个设备相当于一个“数据中心”。

协同编辑

实时协作编辑应用允许多个用户同时编辑文档。

协同编辑和数据库复制有很多相似之处:用户对文档的编辑立即应用到其本地副本,并异步复制到服务器和编辑同一文档的其他用户。

如果想避免编辑发生冲突,则应该对编辑的内容加锁,此时其他用户不能编辑该内容。为了减少锁冲突,锁的粒度应尽可能小:可能是 word 文档的一行内容,也可能是 Excel 的某一单元格。

处理写冲突

多主复制的最大问题是可能发生写冲突

同步与异步冲突检测

理论上, 也可以做到同步冲突检测,即等待写请求完成对所有副本的同步,然后再通知用户写入成功。但是,这样做将会失去多主节点的主要优势:允许每个主节点独立接受写请求。如果确实想要同步方式冲突检测,或许应该考虑采用单主节点的主从复制模型。

避免冲突

处理冲突最理想的策略是避免发生冲突,即如果应用层可以保证对特定记录的写请求总是通过同一个主节点,这样就不会发生写冲突。现实中,由于不少多主节点复制模型所实现的冲突解决方案存在瑕疵,因此,避免冲突反而成为大家普遍推荐的首选方案。

但是,有时可能需要改变事先指定的主节点,例如由于该数据中心发生故障,不得不将流量重新路由到其他数据中心,或者是因为用户已经漫游到另一个位置,因而更靠近新数据中心。此时,冲突避免方式不再有效,必须有措施来处理同时写入冲突的可能性。

收敛于一致状态

对干主从复制模型,数据更新符合顺序性原则,即如果同一个字段有多个更新,则最后一个写操作将决定该字段的最终值。

对于多主节点复制模型,由于不存在这样的写入顺序,所以最终值也会变得不确定。

实现收敛的冲突解决有以下可能的方式:

  • 给每个写入分配唯一的 ID ,例如, 一个时间戳, 二个足够长的随机数, 一个 UUID 或者一个基于键-值的 Jl 合希,优先挑选最高 ID 的写入,并将其他写入丢弃。如果基于时间戳,这种技术被称为最后写入者获胜。虽然这种方陆很流行,但是很容易造成数据丢失。
  • 为每个副本分配一个唯一的 ID ,井制定规则,例如序号高的副本写入始终优先于序号低的副本。这种方法也可能会导致数据丢失。
  • 以某种方式将这些值合并在一起。例如,按字母顺序排序,然后拼接在一起
  • 利用预定义好的格式来记录和保留冲突相关的所有信息,然后依靠应用层的逻辑,事后解决冲突(可能会提示用户) 。

自定义冲突解决逻辑

解决冲突最合适的方式可能还是依靠应用层,所以大多数多主节点复制模型都有工具来让用户编写应用代码来解决冲突。可以在写入时或在读取时执行这些代码逻辑:

  • 在写入时执行:只要数据库系统在复制变更日志时检测到冲突,就会调用应用层的冲突处理程序。
  • 在读取时执行:当检测到冲突时,所有冲突写入值都会暂时保存下来。下一次读取数据时,会将数据的多个版本读返回给应用层。应用层可能会提示用户或自动解决冲突, 井将最后的结果返回到数据库。

什么是冲突?

  • 显而易见的冲突:两个写操作并发地修改了同一条记录中的同一个字段。
  • 微秒的冲突:一个房间接受了两个预定。

拓扑结构

  • 复制拓扑(replication topology)描述写入从一个节点传播到另一个节点的通信路径。
  • 只有两个领导者时,只有一个合理的拓扑:互相写入。
  • 当有两个以上的领导,拓扑很多样:

image.png

  • 最普遍的是全部到全部;
  • MySQL 仅支持环形拓扑。

防止无限复制循环:

  • 圆形和星型拓扑,节点需要转发从其他节点收到的数据更改。
  • 防止无限复制循环:每个节点都有唯一的标识符,在复制日志中,每个写入都标记了所有已经过的节点的标识符。

环形和星形拓扑的问题

  • 一个节点故障,可能中断其他节点之间的复制消息流。
  • 拓扑结构可以重新配置,但是需要手动操作。
  • 全部到全部的容错性更好,避免单点故障。

全部到全部拓扑的问题

  • 网络问题导致消息顺序错乱

image.png

  • 写入时添加时间戳是不够的。
  • 解决办法是版本向量技术
  • 有些数据库没有该功能。

无主复制

客户端将写请求发送到多个节点上,读取时从多个节点上并行读取,以此检测和纠正某些过期数据。

第六章:分区

在不同系统中,分区有着不同的称呼,例如它对应于 MongoDB, Elasticsearch 和 SolrCloud 中的 shard, HBase 的 region, Bigtable 中的 tablet, Cassandra 和 Riak 中的 vnode ,以及 Couch base 中的 vBucket。总体而言,分区是最普遍的术语。

分区定义:每条数据只属于特定分区。

采用数据分区的主要目的是提高可扩展性。不同的分区可以放在一个无共享集群的不同节点上,这样一个大数据集可以分散在更多的节点上,查询负载也随之分散。

对单个分区进行查询时,每个节点对自己所在分区可以独立执行查询操作,因此添加更多的节点可以提高查询吞吐量。超大而复杂的查询尽管比较困难,但也可能做到跨节点的并行处理。

数据分区与数据复制

分区通常与复制结合使用,即每个分区在多个节点都存有副本。这意味着某条记录属于特定的分区,而同样的内容会保存在不同的节点上以提高系统的容错性。

一个节点上可能存储了多个分区。每个分区都有自己的主副本,而从副本则分配在其他一些节点。一个节点可能既是某些分区的主副本,同时又是其他分区的从副本。

键-值数据的分区

分区的主要目标是将数据和查询负载均匀分布在所有节点上。如果节点平均分担负载,那么理论上 10 个节点应该能够处理 10 倍的数据量和 10 倍于单个节点的读写吞吐量(忽略复制) 。

而如果分区不均匀,则会出现某些分区节点比其他分区承担更多的数据量或查询负载,称之为倾斜。倾斜会导致分区效率严重下降,在极端情况下,所有的负载可能会集中在一个分区节点上,这就意味着 10 个节点 9 个空闲,系统的瓶颈在最繁忙的那个节点上。这种负载严重不成比例的分区即成为系统热点。

避免热点最简单的方法是将记录随机分配给所有节点。这种方法的缺点是:当视图读取特定数据时,无法知道数据保存在哪个节点,所以不得不并行查询所有节点。

基于关键字区间分区

一种分区方式是为每个分区分配一段连续的关键字或者关键宇区间范围(以最小值和最大值来指示)。

关键字的区间段不一定非要均匀分布,这主要是因为数据本身可能就不均匀。

分区边界可以由手动选择或自动选择。采用这种分区策略的数据存储有:Bigtable、HBase、RethinkDB、2.4 版本前的 MongoDB。

每个分区内可以按照关键字排序保存。这样可以轻松支持区间查询,即将关键字作为一个拼接起来的索引项从而一次查询得到多个相关记录。

然而,基于关键字的区间分区的缺点是某些访问模式会导致热点。如果关键字是时间戳,则分区对应于一个时间范围。所有的写入操作都集中在同一个分区(即当天的分区),这会导致该分区在写入时负载过高,而其他分区始终处于空闲状态。为了避免上述问题,需要使用时间戳以外的其他内容作为关键字的第一项。

基于关键字晗希值分区

一个好的哈希函数可以处理数据倾斜并使其均匀分布。

用于数据分区目的的哈希函数不需要在加密方面很强。

一且找到合适的关键宇哈希函数,就可以为每个分区分配一个哈希范围(而不是直接作用于关键宇范围),关键字根据其哈希值的范围划分到不同的分区中。

img

这种方总可以很好地将关键字均匀地分配到多个分区中。分区边界可以是均匀间隔,也可以是伪随机选择( 在这种情况下,该技术有时被称为一致性哈希) 。

然而,通过关键宇哈希进行分区,我们丧失了良好的区间查询特性。即使关键字相邻,但经过哈希之后会分散在不同的分区中,区间查询就失去了原有的有序相邻的特性。在 MongoDB 中,如果启用了基于哈希的分片模式,则区间查询会发送到所有的分片上,而 Riak、Couchbase 和 Voldemort 直接就不支持关键字上的区间查询。

Cassandra 中的表可以声明为由多个列组成的复合主键。复合主键只有第一部分可用于哈希分区,而其他列则用作组合索引来对 Cassandra SSTable 中的数据进行排序。

负载倾斜与热点

基于哈希的分区方能可以减轻热点,但无住做到完全避免。一个极端情况是,所有的读/写操作都是针对同一个关键字,则最终所有请求都将被路由到同一个分区。典型:名人的热点事件(关键字是名人的 ID)。

一个简单的规避热点的技术就是:在关键字的开头或结尾处添加一个随机数。只需一个两位数的十进制随机数就可以将关键字的写操作分布到 100 个不同的关键字上,从而分配到不同的分区上。

但是,随之而来的问题是,之后的任何读取都需要些额外的工作,必须从所有 100 个关键字中读取数据然后进行合井。因此通常只对少量的热点关键字附加随机数才有意义;而对于写入吞吐量低的绝大多数关键宇,这些都意味着不必要的开销。此外,还需要额外的元数据来标记哪些关键字进行了特殊处理 。

分区与二级索引

二级索引通常不能唯一标识一条记录,而是用来加速特定值的查询。

二级索引带来的主要挑战是它们不能规整的地映射到分区中。有两种主要的方法来支持对二级索引进行分区:基于文档的分区和基于词条的分区。

基于文档分区的二级索引

img

在这种索引方法中,每个分区完全独立,各自维护自己的二级索引,且只负责自己分区内的文档而不关心其他分区中数据。每当需要写数据库时,包括添加,删除或更新文档等,只需要处理包含目标文档 ID 的那一个分区。因此文档分区索引也被称为本地索引,而不是全局索引。

这种查询分区数据库的方法有时也称为分散/聚集,显然这种二级索引的查询代价高昂。即使采用了并行查询,也容易导致读延迟显著放大。尽管如此,它还是被广泛应用: MongoDB 、Riak、Cassandra、Elasticsearch 、SolrCloud 和 VoltDB 都支持基于文档分区二级索引。

基于词条的二级索引分区

可以对所有的数据构建全局索引,而不是每个分区维护自己的本地索引。

为避免成为瓶颈,不能将全局索引存储在一个节点上,否则就破坏了设计分区均衡的目标。所以,全局索引也必须进行分区,且可以与数据关键字采用不同的分区策略。

img

可以直接通过关键词来全局划分索引,或者对其取哈希值。直接分区的好处是可以支持高效的区间查询;而采用哈希的方式则可以更均匀的划分分区。

词条索引分区 vs. 文档索引分区

  • 优点:它的读取更为高效,客户端不需要对所有的分区都执行一遍查询,只需要向包含词条的那一个分区发出读请求。
  • 缺点:写入速度较慢且非常复杂,主要因为单个文档的更新时,里面可能会涉及多个二级索引,而二级索引的分区又可能完全不同甚至在不同的节点上,由此势必引入显著的写放大。

理想情况下,索引应该时刻保持最新,即写入的数据要立即反映在最新的索引上。但是,对于词条分区来讲,这需要一个跨多个相关分区的分布式事务支持,写入速度会受到极大的影响,所以现有的数据库都不支持同步更新二级索引。对全局 二级索引的更新往往都是异步的

分区再均衡

随着时间的推移,数据库可能总会出现某些变化:

  • 查询压力增加,因此需要更多的 C PU 来处理负载。
  • 数据规模增加,因此需要更多的磁盘和内存来存储数据。
  • 节点可能出现故障,因此需要其他机器来接管失效的节点。

所有这些变化都要求数据和请求可以从一个节点转移到另一个节点。这样一个迁移负载的过程称为再均衡(或者动态均衡)。无论对于哪种分区方案, 分区再均衡通常至少要满足:

  • 均衡之后,负载、数据存储、读写请求等应该在集群范围更均匀地分布
  • 再均衡执行过程中,数据库应该可以继续正常提供读写服务
  • 避免不必要的负载迁移,以加快动态再均衡,井尽量减少网络和磁盘 I/O 影响。

动态再均衡的策略

为什么不用取模?

最好将哈希值划分为不同的区间范围,然后将每个区间分配给一个分区。

对节点数取模方法的问题是:如果节点数 N 发生了变化,会导致很多关键字需要从现有的节点迁移到另一个节点。

固定数量的分区

创建远超实际节点数的分区数,然后为每个节点分配多个分区。

如果集群中添加了一个新节点,该新节点可以从每个现有的节点上匀走几个分区,直到分区再次达到全局平衡。

选中的整个分区会在节点之间迁移,但分区的总数量仍维持不变, 也不会改变关键字到分区的映射关系。这里唯一要调整的是分区与节点的对应关系。考虑到节点间通过网络传输数据总是需要些时间,这样调整可以逐步完成,在此期间,旧的分区仍然可以接收读写请求。

动态分区

对于采用关键字区间分区的数据库,如果边界设置有问题,最终可能会出现所有数据都挤在一个分区而其他分区基本为空,那么设定固定边界、固定数量的分区将非常不便:而手动去重新配置分区边界又非常繁琐。

一些数据库如 HBase 和 RethinkDB 等采用了动态创建分区。当分区的数据增长超过一个可配的参数阔值( HBase 默认值是 10 GB ),它就拆分为两个分区,每个承担一半的数据量;相反,如果大量数据被删除,并且分区缩小到某个阈值以下,则将其与相邻分区进行合井。

每个分区总是分配给一个节点,而每个节点可以承载多个分区,这点与固定数量的分区一样。当一个大的分区发生分裂之后,可以将其中的一半转移到其他某节点以平衡负载。对于 HBase ,分区文件的传输需要借助 HDFS。

需要注意的是,对于一个空的数据库, 因为没有任何先验知识可以帮助确定分区的边界,所以会从一个分区开始。可能数据集很小,但直到达到第一个分裂点之前,所有的写入操作都必须由单个节点来处理, 而其他节点则处于空闲状态。为了缓解这个问题,HBase 和 MongoDB 允许在一个空的数据库上配置一组初始分区(这被称为预分裂)。

按节点比例分区

采用动态分区策略,拆分和合并操作使每个分区的大小维持在设定的最小值和最大值之间,因此分区的数量与数据集的大小成正比关系。另一方面,对于固定数量的分区方式,其每个分区的大小也与数据集的大小成正比。两种情况,分区的数量都与节点数无关。

Cassandra 和 Ketama 则采用了第三种方式,使分区数与集群节点数成正比关系。换句话说,每个节点具有固定数量的分区。此时, 当节点数不变时,每个分区的大小与数据集大小保持正比的增长关系; 当节点数增加时,分区则会调整变得更小。较大的数据量通常需要大量的节点来存储,因此这种方陆也使每个分区大小保持稳定。当一个

新节点加入集群时,它随机选择固定数量的现有分区进行分裂,然后拿走这些分区的一半数据量,将另一半数据留在原节点。随机选择分区边界的前提要求采用基于哈希分区(可以从哈希函数产生的数字范围里设置边界)。

自动与手动再平衡操作

全自动式再平衡会更加方便,它在正常维护之外所增加的操作很少。但是,也有可能出现结果难以预测的情况。再平衡总体讲是个比较昂贵的操作,它需要重新路由请求井将大量数据从一个节点迁移到另一个节点。万一执行过程中间出现异常,会使网络或节点的负载过重,井影响其他请求的性能。

将自动平衡与自动故障检测相结合也可能存在一些风险。例如,假设某个节点负载过重,对请求的响应暂时受到影响,而其他节点可能会得到结论:该节点已经失效;接下来激活自动平衡来转移其负载。客观上这会加重该节点、其他节点以及网络的负荷,可能会使总体情况变得更槽,甚至导致级联式的失效扩散。

请求路由

路由处理策略

  1. 允许客户端链接任意的节点(例如,采用循环式的负载均衡器)。如果某节点恰好拥有所请求的分区,则直接处理该请求:否则,将请求转发到下一个合适的节点,接收答复,并将答复返回给客户端。
  2. 将所有客户端的请求都发送到一个路由层,由后者负责将请求转发到对应的分区节点上。路由层本身不处理任何请求,它仅充一个分区感知的负载均衡器。
  3. 客户端感知分区和节点分配关系。此时,客户端可以直接连接到目标节点,而不需要任何中介。

img

许多分布式数据系统依靠独立的协调服务(如 ZooKeeper )跟踪集群范围内的元数据。每个节点都向 ZooKeeper 中注册自己, ZooKeeper 维护了分区到节点的最终映射关系。其他参与者(如路由层或分区感知的客户端)可以向 ZooKeeper 订阅此信息。一旦分区发生了改变,或者添加、删除节点, ZooKeeper 就会主动通知路由层,这样使路由信息保持最新状态。

img

Linkedln的Espresso使用 Helix进行集群管理(底层是ZooKeeper ), 实现了请求路由 层。 HBase, SolrCloud和Kafka也使用 ZooKeeper来跟踪分区分配情况 。 MongoDB有类似的设计,但它依赖于自己的配置服务器和mongos守护进程来充当路由层。

Cassandra和 Riak 在节点之间使用 gossip协议来同步群集状态的变化。请求可以发送到任何节点,由该节点负责将其转发到目标分区节点。这种方式增加了数据库节点的复杂性,但是避免了对ZooKeeper之类的外部协调服务的依赖。

小结

分区的目地是通过多台机器均匀分布数据和查询负载,避免出现热点。这需要选择合适的数据分区方案,在节点添加或删除时重新动态平衡分区。

  • 基于关键字区间的分区 - 先对关键字进行排序,每个分区只负责一段包含最小到最大关键字范围的一段关键字。对关键字排序的优点是可以支持高效的区间查询,但是如果应用程序经常访问与排序一致的某段关键字,就会存在热点的风险。采用这种方怯,当分区太大时,通常将其分裂为两个子区间,从而动态地再平衡分区。
  • 哈希分区 - 将哈希函数作用于每个关键字,每个分区负责一定范围的哈希值。这种方法打破了原关键字的顺序关系,它的区间查询效率比较低,但可以更均匀地分配负载。采用哈希分区时,通常事先创建好足够多(但固定数量)的分区, 让每个节点承担多个分区,当添加或删除节点时将某些分区从一个节点迁移到另一个节点,也可以支持动态分区。

混合上述两种基本方法也是可行的,例如使用复合键:键的一部分来标识分区,而另一部分来记录排序后的顺序 。

二级索引也需要进行分区,有两种方法:

  • 基于文档来分区二级索引。 二级索引存储在与关键字相 同的分区中 ,这意味着写入时我们只需要更新一个分区,但缺点是读取二级索引时需要在所有分区上执行scatter/gather。
  • 基于词条来分区二级索引。它是基于索引的值而进行的独立分区。二级索引中的条目可能包含来自关键字的多个分区里的记录。在写入时 ,不得不更新二级索引的多个分区;但读取时 ,则可以从单个分区直接快速提取数据。

第七章:事务

事务中的所有读写是一个执行的整体,整事务要么成功(提交)、要么失败(中止或回滚)。如果失败,应用程序可以安全地重试。

深入理解事务

ACID,分别代表原子性( Atomicity ), 一致性( Consistency ),隔离性( Isolation )与持久性( Durability )的首字母。

原子性

原子是指不可分解为更小粒度的东西。

弱隔离级别

串行化

第八章:分布式系统的挑战

所有可能出错的事情一定会出错

分布式系统中可能发生各种问题:

  • 不可靠的网络:当通过网络发送数据包时,数据包可能会丢失或者延迟;同样,回复也可能会丢失或延迟。所以如果没有收到回复,并不能确定消息是否发送成功。
  • 不可靠的时钟:节点的时钟可能会与其他节点存在明显的不同步(尽管尽最大努力设置了 NTP 服务器),时钟还可能会突然向前跳跃或者倒退, 依靠精确的时钟存在一些风险,没有特别简单的办法来精确测量时钟的偏差范围。
  • 进程可能在执行过程中的任意时候遭遇长度未知的暂停( 一个重要的原因是垃圾回收),结果它被其他节点宣告为失效,尽管后来又恢复执行,却对中间的暂停毫无所知。

部分失效可能是分布式系统的关键特征。对于分布式环境,应该具备必要的容错能力。这样即使某些部件发生失效,系统整体还可以继续运行。

为了容忍错误,第一步是检测错误。多数系统没有检测节点是否发生故障的准确机制,因此分布式算法更多依靠超时来确定远程节点是否仍然可用。但是,超时无法区分网络和节点故障,且可变的网络延迟有时会导致节点被误判
为宕机。此外,节点可能处于一种降级状态: 例如,由于驱动程序错误,千兆网络接口可能突然降到 l kb/s 的吞吐量 。这样一个处于“残废”的节点比彻底挂掉的故障节点更难处理。

检测到错误之后,让系统容忍失效也不容易。在典型的分布式环境下,没有全局变量, 没有共享内存,没有约定的尝试或其他跨节点的共享状态。节点甚至不太清楚现在的准确时间, 更不用说其他更高级的了。信息从一个节点流动到另一个节点只能是通过不可靠的网络来发送。单个节点无住安全的做出任何决策,而是需要多个节点之
间的共识协议,井争取达到法定票数(通常为半数以上)。

第九章:一致性与共识

分布式系统最重要的抽象之一就是共识: 所有的节点就某一项提议达成一致。

参考资料

海量数据处理

如何从海量的 URL 中找出相同的 URL?

问题描述

给定 a、b 两个文件,各存放 50 亿个 URL,每个 URL 各占 64B,内存限制是 4G。请找出 a、b 两个文件共同的 URL。

解决思路

每个 URL 占 64B,那么 50 亿个 URL 占用的空间大小约为 320GB。

$$5,000,000,000 * 64 B ≈ 5 GB * 64 = 320 GB$$

由于内存大小只有 4G,因此,我们不可能一次性把所有 URL 加载到内存中处理。对于这种类型的题目,一般采用分治策略,即:把一个文件中的 URL 按照某个特征划分为多个小文件,使得每个小文件大小不超过 4G,这样就可以把这个小文件读到内存中进行处理了。

思路如下:

首先遍历文件 a,对遍历到的 URL 求 hash(URL) % 1000,根据计算结果把遍历到的 URL 存储到 a0, a1, a2, …, a999,这样每个大小约为 300MB。使用同样的方法遍历文件 b,把文件 b 中的 URL 分别存储到文件 b0, b1, b2, …, b999 中。这样处理过后,所有可能相同的 URL 都在对应的小文件中,即 a0 对应 b0, …, a999 对应 b999,不对应的小文件不可能有相同的 URL。那么接下来,我们只需要求出这 1000 对小文件中相同的 URL 就好了。

接着遍历 ai( i∈[0,999]),把 URL 存储到一个 HashSet 集合中。然后遍历 bi 中每个 URL,看在 HashSet 集合中是否存在,若存在,说明这就是共同的 URL,可以把这个 URL 保存到一个单独的文件中。

方案总结

  • 分而治之,进行哈希取余;
  • 对每个子文件进行 HashSet 统计。

如何从海量数据中找出高频词?

问题描述

有一个 1GB 大小的文件,文件里每一行是一个词,每个词的大小不超过 16B,内存大小限制是 1MB,要求返回频数最高的 100 个词(Top 100)。

解决思路

由于内存限制,无法直接将大文件的所有词一次读到内存中。因此,可以采用分治策略,把一个大文件分解成多个小文件,保证每个文件的大小小于 1MB,进而直接将单个小文件读取到内存中进行处理。

思路如下

首先遍历大文件,对遍历到的每个词 x,执行 hash(x) % 5000,将结果为 i 的词存放到文件 Ai 中。遍历结束后,我们可以得到 5000 个小文件。每个小文件的大小为 200KB 左右。如果有的小文件大小仍然超过 1MB,则采用同样的方式继续进行分解。

接着统计每个小文件中出现频数最高的 100 个词。最简单的方式是使用 HashMap 来实现。其中 key 为词,value 为该词出现的频率。具体方法是:对于遍历到的词 x,如果在 map 中不存在,则执行 map.put(x, 1) 若存在,则执行 map.put(x, map.get(x)+1),将该词频数加 1。

上面我们统计了每个小文件单词出现的频数。接下来,我们可以通过维护一个小顶堆来找出所有词中出现频数最高的 100 个。具体方法是:依次遍历每个小文件,构建一个小顶堆,堆大小为 100。如果遍历到的词的出现次数大于堆顶词的出现次数,则用新词替换堆顶的词,然后重新调整为小顶堆,遍历结束后,小顶堆上的词就是出现频数最高的 100 个词。

方案总结

  • 分而治之,进行哈希取余;
  • 使用 HashMap 统计频数;
  • 求解最大的 TopN 个,用小顶堆;求解最小的 TopN 个,用大顶堆

如何找出某一天访问百度网站最多的 IP?

问题描述

现有海量日志数据保存在一个超大文件中,该文件无法直接读入内存,要求从中提取某天访问百度次数最多的那个 IP。

解决思路

这道题只关心某一天访问百度最多的 IP,因此,可以首先对文件进行一次遍历,把这一天访问百度 IP 的相关信息记录到一个单独的大文件中。接下来采用的方法与上一题一样,大致就是先对 IP 进行哈希映射,接着使用 HashMap 统计重复 IP 的次数,最后计算出重复次数最多的 IP。

注:这里只需要找出出现次数最多的 IP,可以不必使用堆,直接用一个变量 max 即可。

方法总结

  • 分而治之,进行哈希取余;
  • 使用 HashMap 统计频数;
  • 求解最大的 TopN 个,用小顶堆;求解最小的 TopN 个,用大顶堆

如何在大量的数据中找出不重复的整数?

问题描述

在 2.5 亿个整数中找出不重复的整数。注意:内存不足以容纳这 2.5 亿个整数。

解决思路

方法一:分治法

与前面的题目方法类似,先将 2.5 亿个数划分到多个小文件,用 HashSet/HashMap 找出每个小文件中不重复的整数,再合并每个子结果,即为最终结果。

方法二:位图法

位图,就是用一个或多个 bit 来标记某个元素对应的值,而键就是该元素。采用位作为单位来存储数据,可以大大节省存储空间。

位图通过使用位数组来表示某些元素是否存在。它可以用于快速查找,判重,排序等。不是很清楚?我先举个小例子。

假设我们要对 [0,7] 中的 5 个元素 (6, 4, 2, 1, 5) 进行排序,可以采用位图法。0~7 范围总共有 8 个数,只需要 8bit,即 1 个字节。首先将每个位都置 0:

1
0 0 0 0 0 0 0 0

然后遍历 5 个元素,首先遇到 6,那么将下标为 6 的位的 0 置为 1;接着遇到 4,把下标为 4 的位 的 0 置为 1:

1
0 0 0 0 1 0 1 0

依次遍历,结束后,位数组是这样的:

1
0 1 1 0 1 1 1 0

每个为 1 的位,它的下标都表示了一个数:

1
2
3
for i in range(8):
if bits[i] == 1:
print(i)

这样我们其实就已经实现了排序。

对于整数相关的算法的求解,位图法是一种非常实用的算法。假设 int 整数占用 4B,即 32bit,那么我们可以表示的整数的个数为 232。

那么对于这道题,我们用 2 个 bit 来表示各个数字的状态:

  • 00 表示这个数字没出现过;
  • 01 表示这个数字出现过一次(即为题目所找的不重复整数);
  • 10 表示这个数字出现了多次。

那么这 232 个整数,总共所需内存为 232*2b=1GB。因此,当可用内存超过 1GB 时,可以采用位图法。假设内存满足位图法需求,进行下面的操作:

遍历 2.5 亿个整数,查看位图中对应的位,如果是 00,则变为 01,如果是 01 则变为 10,如果是 10 则保持不变。遍历结束后,查看位图,把对应位是 01 的整数输出即可。

方法总结

判断数字是否重复的问题,位图法是一种非常高效的方法。

如何在大量的数据中判断一个数是否存在?

题目描述

给定 40 亿个不重复的没排过序的 unsigned int 型整数,然后再给定一个数,如何快速判断这个数是否在这 40 亿个整数当中?

解答思路

方法一:分治法

依然可以用分治法解决,方法与前面类似,就不再次赘述了。

方法二:位图法

40 亿个不重复整数,我们用 40 亿个 bit 来表示,初始位均为 0,那么总共需要内存:4,000,000,000b≈512M。

我们读取这 40 亿个整数,将对应的 bit 设置为 1。接着读取要查询的数,查看相应位是否为 1,如果为 1 表示存在,如果为 0 表示不存在。

方法总结

判断数字是否存在、判断数字是否重复的问题,位图法是一种非常高效的方法。

如何查询最热门的查询串?

题目描述

搜索引擎会通过日志文件把用户每次检索使用的所有查询串都记录下来,每个查询串的长度不超过 255 字节。

假设目前有 1000w 个记录(这些查询串的重复度比较高,虽然总数是 1000w,但如果除去重复后,则不超过 300w 个)。请统计最热门的 10 个查询串,要求使用的内存不能超过 1G。(一个查询串的重复度越高,说明查询它的用户越多,也就越热门。)

解答思路

每个查询串最长为 255B,1000w 个串需要占用 约 2.55G 内存,因此,我们无法将所有字符串全部读入到内存中处理。

方法一:分治法

分治法依然是一个非常实用的方法。

划分为多个小文件,保证单个小文件中的字符串能被直接加载到内存中处理,然后求出每个文件中出现次数最多的 10 个字符串;最后通过一个小顶堆统计出所有文件中出现最多的 10 个字符串。

方法可行,但不是最好,下面介绍其他方法。

方法二:HashMap 法

虽然字符串总数比较多,但去重后不超过 300w,因此,可以考虑把所有字符串及出现次数保存在一个 HashMap 中,所占用的空间为 300w*(255+4)≈777M(其中,4 表示整数占用的 4 个字节)。由此可见,1G 的内存空间完全够用。

思路如下

首先,遍历字符串,若不在 map 中,直接存入 map,value 记为 1;若在 map 中,则把对应的 value 加 1,这一步时间复杂度 O(N)

接着遍历 map,构建一个 10 个元素的小顶堆,若遍历到的字符串的出现次数大于堆顶字符串的出现次数,则进行替换,并将堆调整为小顶堆。

遍历结束后,堆中 10 个字符串就是出现次数最多的字符串。这一步时间复杂度 O(Nlog10)

方法三:前缀树法(字典树)

方法二使用了 HashMap 来统计次数,当这些字符串有大量相同前缀时,可以考虑使用前缀树来统计字符串出现的次数,树的结点保存字符串出现次数,0 表示没有出现。

思路如下

在遍历字符串时,在前缀树中查找,如果找到,则把结点中保存的字符串次数加 1,否则为这个字符串构建新结点,构建完成后把叶子结点中字符串的出现次数置为 1。

最后依然使用小顶堆来对字符串的出现次数进行排序。

方法总结

前缀树经常被用来统计字符串的出现次数。它的另外一个大的用途是字符串查找,判断是否有重复的字符串等。

如何统计不同电话号码的个数?

题目描述

已知某个文件内包含一些电话号码,每个号码为 8 位数字,统计不同号码的个数。

解答思路

这道题本质还是求解数据重复的问题,对于这类问题,一般首先考虑位图法。

对于本题,8 位电话号码可以表示的号码个数为 $$10^8$$ 个,即 1 亿个。我们每个号码用一个 bit 来表示,则总共需要 1 亿个 bit,内存占用约 100M。

思路如下

申请一个位图数组,长度为 1 亿,初始化为 0。然后遍历所有电话号码,把号码对应的位图中的位置置为 1。遍历完成后,如果 bit 为 1,则表示这个电话号码在文件中存在,否则不存在。bit 值为 1 的数量即为 不同电话号码的个数。

方法总结

求解数据重复问题,记得考虑位图法。

如何从 5 亿个数中找出中位数?

题目描述

从 5 亿个数中找出中位数。数据排序后,位置在最中间的数就是中位数。当样本数为奇数时,中位数为 第 (N+1)/2 个数;当样本数为偶数时,中位数为 第 N/2 个数与第 1+N/2 个数的均值。

解答思路

如果这道题没有内存大小限制,则可以把所有数读到内存中排序后找出中位数。但是最好的排序算法的时间复杂度都为 O(NlogN)。这里使用其他方法。

方法一:双堆法

维护两个堆,一个大顶堆,一个小顶堆。大顶堆中最大的数小于等于小顶堆中最小的数;保证这两个堆中的元素个数的差不超过 1。

若数据总数为偶数,当这两个堆建好之后,中位数就是这两个堆顶元素的平均值。当数据总数为奇数时,根据两个堆的大小,中位数一定在数据多的堆的堆顶

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
class MedianFinder {

private PriorityQueue<Integer> maxHeap;
private PriorityQueue<Integer> minHeap;

/** initialize your data structure here. */
public MedianFinder() {
maxHeap = new PriorityQueue<>(Comparator.reverseOrder());
minHeap = new PriorityQueue<>(Integer::compareTo);
}

public void addNum(int num) {
if (maxHeap.isEmpty() || maxHeap.peek() > num) {
maxHeap.offer(num);
} else {
minHeap.offer(num);
}

int size1 = maxHeap.size();
int size2 = minHeap.size();
if (size1 - size2 > 1) {
minHeap.offer(maxHeap.poll());
} else if (size2 - size1 > 1) {
maxHeap.offer(minHeap.poll());
}
}

public double findMedian() {
int size1 = maxHeap.size();
int size2 = minHeap.size();

return size1 == size2
? (maxHeap.peek() + minHeap.peek()) * 1.0 / 2
: (size1 > size2 ? maxHeap.peek() : minHeap.peek());
}
}

见 LeetCode No.295:https://leetcode.com/problems/find-median-from-data-stream/

以上这种方法,需要把所有数据都加载到内存中。当数据量很大时,就不能这样了,因此,这种方法适用于数据量较小的情况。5 亿个数,每个数字占用 4B,总共需要 2G 内存。如果可用内存不足 2G,就不能使用这种方法了,下面介绍另一种方法。

方法二:分治法

分治法的思想是把一个大的问题逐渐转换为规模较小的问题来求解。

对于这道题,顺序读取这 5 亿个数字,对于读取到的数字 num,如果它对应的二进制中最高位为 1,则把这个数字写到 f1 中,否则写入 f0 中。通过这一步,可以把这 5 亿个数划分为两部分,而且 f0 中的数都大于 f1 中的数(最高位是符号位)。

划分之后,可以非常容易地知道中位数是在 f0 还是 f1 中。假设 f1 中有 1 亿个数,那么中位数一定在 f0 中,且是在 f0 中,从小到大排列的第 1.5 亿个数与它后面的一个数的平均值。

提示,5 亿数的中位数是第 2.5 亿与右边相邻一个数求平均值。若 f1 有一亿个数,那么中位数就是 f0 中从第 1.5 亿个数开始的两个数求得的平均值。

对于 f0 可以用次高位的二进制继续将文件一分为二,如此划分下去,直到划分后的文件可以被加载到内存中,把数据加载到内存中以后直接排序,找出中位数。

注意,当数据总数为偶数,如果划分后两个文件中的数据有相同个数,那么中位数就是数据较小的文件中的最大值与数据较大的文件中的最小值的平均值。

方法总结

分治法,真香!

如何找出排名前 500 的数?

题目描述

有 20 个数组,每个数组有 500 个元素,并且有序排列。如何在这 20*500 个数中找出前 500 的数?

解答思路

对于 TopK 问题,最常用的方法是使用堆排序。对本题而言,假设数组降序排列,可以采用以下方法:

首先建立大顶堆,堆的大小为数组的个数,即为 20,把每个数组最大的值存到堆中。

接着删除堆顶元素,保存到另一个大小为 500 的数组中,然后向大顶堆插入删除的元素所在数组的下一个元素。

重复上面的步骤,直到删除完第 500 个元素,也即找出了最大的前 500 个数。

为了在堆中取出一个数据后,能知道它是从哪个数组中取出的,从而可以从这个数组中取下一个值,可以把数组的指针存放到堆中,对这个指针提供比较大小的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
import lombok.Data;

import java.util.Arrays;
import java.util.PriorityQueue;

/**
* @author https://github.com/yanglbme
*/
@Data
public class DataWithSource implements Comparable<DataWithSource> {
/**
* 数值
*/
private int value;

/**
* 记录数值来源的数组
*/
private int source;

/**
* 记录数值在数组中的索引
*/
private int index;

public DataWithSource(int value, int source, int index) {
this.value = value;
this.source = source;
this.index = index;
}

/**
*
* 由于 PriorityQueue 使用小顶堆来实现,这里通过修改
* 两个整数的比较逻辑来让 PriorityQueue 变成大顶堆
*/
@Override
public int compareTo(DataWithSource o) {
return Integer.compare(o.getValue(), this.value);
}
}


class Test {
public static int[] getTop(int[][] data) {
int rowSize = data.length;
int columnSize = data[0].length;

// 创建一个columnSize大小的数组,存放结果
int[] result = new int[columnSize];

PriorityQueue<DataWithSource> maxHeap = new PriorityQueue<>();
for (int i = 0; i < rowSize; ++i) {
// 将每个数组的最大一个元素放入堆中
DataWithSource d = new DataWithSource(data[i][0], i, 0);
maxHeap.add(d);
}

int num = 0;
while (num < columnSize) {
// 删除堆顶元素
DataWithSource d = maxHeap.poll();
result[num++] = d.getValue();
if (num >= columnSize) {
break;
}

d.setValue(data[d.getSource()][d.getIndex() + 1]);
d.setIndex(d.getIndex() + 1);
maxHeap.add(d);
}
return result;

}

public static void main(String[] args) {
int[][] data = {
{29, 17, 14, 2, 1},
{19, 17, 16, 15, 6},
{30, 25, 20, 14, 5},
};

int[] top = getTop(data);
System.out.println(Arrays.toString(top)); // [30, 29, 25, 20, 19]
}
}

方法总结

求 TopK,不妨考虑一下堆排序?

如何按照 query 的频度排序?

题目描述

有 10 个文件,每个文件大小为 1G,每个文件的每一行存放的都是用户的 query,每个文件的 query 都可能重复。要求按照 query 的频度排序。

解答思路

如果 query 的重复度比较大,可以考虑一次性把所有 query 读入内存中处理;如果 query 的重复率不高,那么可用内存不足以容纳所有的 query,这时候就需要采用分治法或其他的方法来解决。

方法一:HashMap 法

如果 query 重复率高,说明不同 query 总数比较小,可以考虑把所有的 query 都加载到内存中的 HashMap 中。接着就可以按照 query 出现的次数进行排序。

方法二:分治法

分治法需要根据数据量大小以及可用内存的大小来确定问题划分的规模。对于这道题,可以顺序遍历 10 个文件中的 query,通过 Hash 函数 hash(query) % 10 把这些 query 划分到 10 个小文件中。之后对每个小文件使用 HashMap 统计 query 出现次数,根据次数排序并写入到零外一个单独文件中。

接着对所有文件按照 query 的次数进行排序,这里可以使用归并排序(由于无法把所有 query 都读入内存,因此需要使用外排序)。

方法总结

  • 内存若够,直接读入进行排序;
  • 内存不够,先划分为小文件,小文件排好序后,整理使用外排序进行归并。

《极客时间教程 - 从 0 开始学微服务》笔记

到底什么是微服务?

微服务定义

微服务是由单一应用程序构成的小服务,拥有自己的进程与轻量化处理,服务依业务功能设计,以全自动的方式部署,与其他服务使用 HTTP API 通讯。同时,服务会使用最小规模的集中管理 (例如 Docker)技术,服务可以用不同的编程语言与数据库等。

——Martin Fowler 和 James Lewis

单体应用的问题

  • 部署效率低
  • 团队协作开发成本高
  • 单点故障问题
  • 线上发布变慢

服务化:本地方法调用 转为 远程方法调用(RPC)

微服务和服务化的差异:

  • 服务拆分粒度更细
  • 服务独立部署、维护
  • 服务治理要求高

从单体应用走向服务化

什么时候进行服务化拆分?

经验:开发人员超过 10 人(沟通成本变高),就可以考虑服务化拆分

服务化拆分的两种姿势

纵向拆分,从业务维度进行拆分。标准是按照业务的关联程度来决定,关联比较密切的业务适合拆分为一个微服务,而功能相对比较独立的业务适合单独拆分为一个微服务。

横向拆分,从公共且独立功能维度拆分。标准是按照是否有公共的被多个其他服务调用,且依赖的资源独立不与其他业务耦合。

服务化拆分的前置条件

  • 服务如何定义。通过接口来约定。
  • 服务如何发布和订阅。通过服务注册和发现。
  • 服务如何监控故障如何定位。服务化需要链路监控。
  • 服务如何治理。超时和重试、流量控制。

初探微服务架构

微服务通过注册中心,实现发布订阅模式。

服务调用主要依赖几个基本组件:

  • 服务描述:常用的服务描述方式包括 RESTful API、XML 配置以及 IDL 文件三种。
    • RESTful API 代表:Swagger
    • XML 代表:Dubbo
    • IDL 代表:Thrift、gRPC
  • 注册中心
    • 服务提供者在启动时,根据服务发布文件中配置的发布信息向注册中心注册自己的服务。
    • 服务消费者在启动时,根据消费者配置文件中配置的服务信息向注册中心订阅自己所需要的服务。
    • 注册中心返回服务提供者地址列表给服务消费者。
    • 当服务提供者发生变化,比如有节点新增或者销毁,注册中心将变更通知给服务消费者。
  • 服务框架
    • 通信协议:选择 TCP、UDP、HTTP,还是其他?
    • 数据传输方式:同步、异步、多路复用?
    • 序列化方式:JDK 序列化、Json、二进制(Protobuf、Thrift)?
  • 服务监控
    • 数据采集
    • 数据处理
    • 数据展示
  • 服务追踪
  • 工作原理:通过 requestId、spanId 分别表示一次请求、请求中的某一环节
  • 服务治理:
    • 超时、重试
    • 负载均衡
    • 故障转移
    • 流量控制

如何发布和引用服务?

RESTful API:主要被用作 HTTP 或者 HTTPS 协议的接口定义。代表:Eureka

XML 配置:代表:Dubbo。工作步骤:

  • 服务提供者定义接口,并实现接口。
  • 服务提供者进程启动时,通过加载 server.xml 配置文件将接口暴露出去。
  • 服务消费者进程启动时,通过加载 client.xml 配置文件来引入要调用的接口。

IDL 文件:IDL 就是接口描述语言(interface description language)的缩写。主要用作跨语言平台的服务之间的调用。有两种最常用的 IDL:Thrift、gRPC。

如何注册和发现服务?

微服务架构下,主要有三种角色:

  • 服务提供者(RPC Server)
  • 服务消费者(RPC Client)
  • 服务注册中心(Registry)

注册中心实现方式

注册中心必须提供以下最基本的 API,例如:

  • 服务注册接口

  • 服务注销接口

  • 心跳汇报接口

  • 服务订阅接口:服务消费者通过调用服务订阅接口完成服务订阅,获取可用的服务提供者节点列表。

  • 服务变更查询接口

  • 服务查询接口

  • 服务修改接口

集群部署

注册中心一般都是采用集群部署来保证高可用性,并通过分布式一致性协议来确保集群中不同节点之间的数据保持一致。

以 ZooKeeper 的工作原理为例:

  • 每个 Server 在内存中存储了一份数据,Client 的读请求可以请求任意一个 Server。
  • ZooKeeper 启动时,将从实例中选举一个 leader(Paxos 协议)。
  • Leader 负责处理数据更新等操作(ZAB 协议)。
  • 一个更新操作成功,当且仅当大多数 Server 在内存中成功修改 。

目录存储

注册中心存储服务信息一般采用层次化的目录结构:

  • 每个目录在 ZooKeeper 中叫作 znode,并且其有一个唯一的路径标识。
  • znode 可以包含数据和子 znode。
  • znode 中的数据可以有多个版本,比如某一个 znode 下存有多个数据版本,那么查询这个路径下的数据需带上版本信息。

服务健康状态检测

ZooKeeper 客户端和服务端维持的是一个长连接。连接成功后,会生成一个全局唯一的 Session ID,客户端定期发送心跳消息,服务端收到后重置会话超时时间。如果超时,则认为连接结束。

如果一个服务将 ZooKeeper 作为服务注册中心,一旦连接超时,ZooKeeper 会认为这个服务节点已经不可用,就会将其信息删除。

服务状态变更通知

ZooKeeper 支持 Watch 机制。服务消费者可以监听服务提供者的节点信息。一旦服务提供者的节点信息哟变化,就可以获取到变更状态。

白名单机制

通常注册中心会有多套环境,区分开发、测试、线上等环境。如果弄错了,会出现意想不到的后果,为此需要引入白名单保护机制。只有添加到注册中心白名单内的 RPC Server,才能够调用注册中心的注册接口,这样的话可以避免测试环境中的节点意外跑到线上环境中去。

如何实现 RPC 远程服务调用?

客户端和服务端如何建立网络连接?

  • HTTP 通信:三次握手建立连接;四次挥手断开连接
  • Socket 通信
    • 服务器监听
    • 客户端请求
    • 连接确认
    • 数据传输

服务端如何处理请求?

  • BIO
  • NIO
  • AIO

数据传输采用什么协议?

  • Http
  • Dubbo

数据该如何序列化和反序列化?

  • JDK
  • JSON
  • 二进制(PB、Thrift 等)

如何监控微服务调用?

监控对象

  • 客户端监控
  • 接口监控
  • 资源监控
  • 基础监控

监控指标

  • 请求量
  • 响应时间
  • 错误率

监控维度

  • 全局维度
  • 机房维度
  • 单机维度
  • 时间维度
  • 重要性维度

监控关键点

  • 数据采集
    • 主动上报
    • 代理收集
  • 数据传输
    • UDP
    • Kafka
  • 数据处理
    • 全文检索:如 Elasticsearch
    • 时序数据库:如 InfluxDB、OpenTSDB
    • 流计算:如 Spark、Storm、Flink
  • 数据展示

如何追踪微服务调用?

服务追踪的作用

  • 定位整个系统的瓶颈点
  • 优化链路调用
  • 生成网络拓扑
  • 透明传输数据

服务追踪系统原理

经典论文:Dapper, a Large-Scale Distributed Systems Tracing Infrastructure

  • traceId,用于标识某一次具体的请求 ID。当用户的请求进入系统后,会在 RPC 调用网络的第一层生成一个全局唯一的 traceId,并且会随着每一层的 RPC 调用,不断往后传递,这样的话通过 traceId 就可以把一次用户请求在系统中调用的路径串联起来。
  • spanId,用于标识一次 RPC 调用在分布式请求中的位置。当用户的请求进入系统后,处在 RPC 调用网络的第一层 A 时 spanId 初始值是 0,进入下一层 RPC 调用 B 的时候 spanId 是 0.1,继续进入下一层 RPC 调用 C 时 spanId 是 0.1.1,而与 B 处在同一层的 RPC 调用 E 的 spanId 是 0.2,这样的话通过 spanId 就可以定位某一次 RPC 请求在系统调用中所处的位置,以及它的上下游依赖分别是谁。
  • annotation,用于业务自定义埋点数据,可以是业务感兴趣的想上传到后端的数据,比如一次请求的用户 UID。

服务追踪系统实现

服务追踪系统可以分为三层。

  • 数据采集层,负责数据埋点并上报。
  • 数据处理层,负责数据的存储与计算。
  • 数据展示层,负责数据的图形化展示。

微服务治理的手段有哪些?

服务调用失败原因:

  • 服务提供者自身问题,如宕机、进程退出等;
  • 网络问题

节点管理

  • 注册中心主动摘除机制:服务提供者定时发送心跳,如果超时,注册中心把节点从服务列表中删除
  • 服务消费者摘除机制:如果服务消费者调用服务提供者节点失败,就将这个节点从内存中保存的可用服务提供者节点列表中移除。

负载均衡

  • 随机算法
  • 轮询算法
  • 最少活跃调用算法
  • 一致性 Hash 算法

服务路由

为什么要制定路由规则呢?

  • 业务存在灰度发布的需求
  • 多机房就近访问的需求

如何配置路由规则

  • 静态配置:修改服务消费者本地配置,上线后生效
  • 动态配置:修改注册中心的配置,服务消费者在下一个同步周期之后,就会动态更新

服务容错

  • FailOver:失败自动切换。
  • FailBack:失败通知。
  • FailCache:失败缓存。
  • FailFast:快速失败。

一般情况下对于幂等的调用,可以选择 FailOver 或者 FailCache,非幂等的调用可以选择 FailBack 或者 FailFast。

Dubbo 框架里的微服务组件

服务发布和引用的实践

XML 配置方式的服务发布和引用流程

  • 服务提供者定义接口
  • 服务提供者发布接口
  • 服务消费者引用接口

服务发布和引用的那些坑

如何将注册中心落地?

注册中心如何存储服务信息

服务一般会分成多个不同的分组

  • 核心与非核心,从业务的核心程度来分。
  • 机房,从机房的维度来分。
  • 线上环境与测试环境,从业务场景维度来区分。

所以注册中心存储的服务信息一般包含三部分内容:分组服务名以及节点信息,节点信息又包括节点地址和节点其他信息。

注册中心工作流程

  • 服务提供者注册流程。
  • 服务提供者反注册流程。
  • 服务消费者查询流程。
  • 服务消费者订阅变更流程。

如何注册节点

  • 首先查看要注册的节点是否在白名单内?如果不在就抛出异常,在的话继续下一步。
  • 其次要查看注册的 Cluster(服务的接口名)是否存在?如果不存在就抛出异常,存在的话继续下一步。
  • 然后要检查 Service(服务的分组)是否存在?如果不存在则抛出异常,存在的话继续下一步。
  • 最后将节点信息添加到对应的 Service 和 Cluster 下面的存储中。

如何反注册

  • 查看 Service(服务的分组)是否存在,不存在就抛出异常,存在就继续下一步。
  • 查看 Cluster(服务的接口名)是否存在,不存在就抛出异常,存在就继续下一步。
  • 删除存储中 Service 和 Cluster 下对应的节点信息。
  • 更新 Cluster 的 sign 值。

如何查询节点信息

首先从 localcache(本机内存)中查找,如果没有就继续下一步。

接着从 snapshot(本地快照)中查找,如果没有就继续下一步。

如何订阅服务变更

  • 服务消费者从注册中心获取了服务的信息后,就订阅了服务的变化,会在本地保留 Cluster 的 sign 值。
  • 服务消费者每隔一段时间,调用 getSign() 函数,从注册中心获取服务端该 Cluster 的 sign 值,并与本地保留的 sign 值做对比,如果不一致,就从服务端拉取新的节点信息,并更新 localcache 和 snapshot。

注册与发现的几个问题

  • 多注册中心

  • 并行订阅服务

  • 批量反注册服务

  • 服务变更信息增量更新

开源服务注册中心如何选型?

  • 应用内注册与发现:注册中心提供服务端和客户端的 SDK,业务应用通过引入注册中心提供的 SDK,通过 SDK 与注册中心交互,来实现服务的注册和发现。典型代表:Eureka
  • 应用外注册与发现:业务应用本身不需要通过 SDK 与注册中心打交道,而是通过其他方式与注册中心交互,间接完成服务注册与发现。典型代表:Consul

二者对比:

  • 用内的解决方案一般适用于服务提供者和服务消费者同属于一个技术体系;
  • 应用外的解决方案一般适合服务提供者和服务消费者采用了不同技术体系的业务场景

注册中心选型要考虑的两个问题

  • 高可用性
  • 数据一致性
    • CP 型:牺牲可用性来保证数据强一致性。代表:ZooKeeper、Etcd、Consul
    • AP 型:代表:Eureka、Nacos

而对于注册中心来说,最主要的功能是服务的注册和发现,在网络出现问题的时候,可用性的需求要远远高于数据一致性。即使因为数据不一致,注册中心内引入了不可用的服务节点,也可以通过其他措施来避免,比如客户端的快速失败机制等,只要实现最终一致性,对于注册中心来说就足够了。因此,选择 AP 型注册中心,一般更加合适。

开源 RPC 框架如何选型?

限定语言 RPC

  • Dubbo:仅支持 Java
  • Motan:仅支持 Java
  • Tars:仅支持 C++
  • Spring Cloud:仅支持 Java

跨语言 RPC

  • gRPC:支持 C++、Java、Python、Go、Ruby、PHP、Android Java、Objective-C 等多种语言
  • Thrift:支持 C++、Java、PHP、Python、Ruby、Erlang 等多种语言

如何搭建一个可靠的监控系统?

日志解决方案:ELK

时序数据库解决方案:GraphiteTICKPrometheus

如何搭建一套适合你的服务追踪系统?

代表:Zipkin、PinPoint

如何识别服务节点是否存活?

心跳开关保护机制

问题:服务消费者同时并发访问注册中心获取最新服务信息导致注册中心带宽被打满

方案:需要一种保护机制,即使在网络频繁抖动的时候,服务消费者也不至于同时去请求注册中心获取最新的服务节点信息。

服务节点摘除保护机制

问题:服务提供者节点被大量摘除导致服务消费者没有足够的节点可以调用

方案:需要根据实际业务的情况,设定一个阈值比例,即使遇到刚才说的这种情况,注册中心也不能摘除超过这个阈值比例的节点。

静态注册中心

如何使用负载均衡算法?

负载均衡算法

  • 随机算法

  • 轮询算法

  • 加权轮询算法

  • 最少活跃连接算法

  • 一致性 hash 算法

如何使用服务路由?

服务路由就是服务消费者在发起服务调用时,必须根据特定的规则来选择服务节点,从而满足某些特定的需求

服务路由的应用场景

  • 分组调用
  • 灰度发布
  • 流量切换
  • 读写分离

服务路由的规则

  • 条件路由
    • 排除某个服务节点
    • 白名单和黑名单功能
    • 机房隔离
    • 读写分离
  • 脚本路由

服务路由的获取方式

  • 本地配置
  • 配置中心管理
  • 动态下发

服务端出现故障时该如何应对?

微服务故障种类

  • 集群故障。解决:流量控制
    • 限流
    • 降级
  • 单 IDC 故障。解决:多 IDC 部署、流量切换
    • 多 IDC 部署
      • 同城多活
      • 异地多活
    • 流量切换
      • DNS 解析流量切换
      • RPC 流量切换
  • 单机故障

服务调用失败时有哪些处理手段?

超时

重试

流量控制

如何管理服务配置?

配置类型:

  • 本地配置
  • 配置中心

配置中心代表:

如何搭建微服务治理平台?

服务管理

  • 服务上下线
  • 节点添加 / 删除
  • 服务查询
  • 服务节点查询。这个操作会调用注册中心的节点查询接口,来查询某个服务下一共有多少个节点。

服务治理

  • 限流
  • 降级
  • 切流量

服务监控

问题定位

日志查询

服务运维

  • 发布部署
  • 弹性伸缩

微服务架构该如何落地?

(略)

微服务为什么要容器化?

微服务引入的问题

设计复杂

测试复杂

运维困难

微服务容器化运维:镜像仓库和资源调度

容器运维平台的组成部分

  • 镜像仓库
  • 资源调度
  • 容器调度
  • 服务编排

微服务容器化运维:容器调度和服务编排

容器调度系统代表:SwarmMesosKubernetes

容器调度要解决的问题

  • 主机过滤
    • 存活过滤
    • 硬件过滤
  • 调度策略
  • 服务编排
  • 服务依赖:代表方案:Docker Compose
  • 服务发现
    • 基于 Nginx 的服务发现
    • 基于注册中心的服务发现
    • 弹性伸缩

微服务容器化运维:微博容器运维平台 DCP

微服务如何实现 DevOps?

  • CI(Continuous Integration),持续集成。开发完成代码开发后,能自动地进行代码检查、单元测试、打包部署到测试环境,进行集成测试,跑自动化测试用例。
    • 代码检查
    • 单元测试
    • 集成测试
  • CD(Continuous Deploy),持续部署。代码测试通过后,能自动部署到类生产环境中进行集成测试,测试通过后再进行小流量的灰度验证,验证通过后代码就达到线上发布的要求了,就可以把代码自动部署到线上。

如何做好微服务容量规划?

微服务容量规划的问题

  • 服务数量众多
  • 服务的接口表现差异巨大
  • 服务部署的集群规模大小不同
  • 服务之间还存在依赖关系

容量规划系统的作用是根据各个微服务部署集群的最大容量和线上实际运行的负荷,来决定各个微服务是否需要弹性扩缩容,以及需要扩缩容多少台机器

容量规划系统实施的关键在于两点:

  • 容量评估
    • 选择合适的压测指标
      • 系统类指标:CPU、内存、I/O、带宽等
      • 服务类指标:响应时间、P999 耗时、错误率等
    • 压测获取单机的最大容量
      • 单机压测
        • 通过日志回放等手段,模拟线上流量来对单机进行压测;
        • 通过 TCP-Copy 的方式,把线上机器的流量拷贝过来对单机进行压测。
      • 集群压测
    • 实时和获取集群的运行负荷
  • 调度决策
    • 可以使用水位线来进行调度决策:当集群的水位线位于致命线以下时,就需要立即扩容,在扩容一定数量的机器后,水位线回到安全线以上并保持一段时间后,就可以进行缩容了。
    • 扩容
      • 按数量
      • 按比例
    • 缩容
    • 逐步缩容
    • 为了避免因扩容、缩容导致的水位线抖动,可以多次采集水位线数据,超过 60% 数据满足库哦哦让条件,才真正触发扩容。

微服务多机房部署实践

多机房负载均衡:利用七层负载均衡和四层负载均衡,将流量根据用户就近访问的原则切分流量。

多机房数据同步

主从机房架构

  • 由主机房的处理机来更新本机房的缓存和数据库
  • 其他机房的缓存也通过主机房的处理机来更新
  • 从机房的数据库则通过 MySQL 的 binlog 同步主机房的数据。

独立机房架构

  • 每个机房的处理机接收到写请求后更新各自机房的缓存
  • 只有主机房会更新数据库
  • 从机房的数据库则通过 MySQL 的 binlog 同步主机房的数据。

WMB 消息同步组件的功能就是把一个机房的写请求发给另外一个机房

  • reship,负责把本机房的写请求分发一份给别的机房。
  • collector,负责从别的机房读取写请求,然后再把请求转发给本机房的处理机。

实现 WMB 的消息同步功能有两种方案:

  • MQ:两个机房的 MQ 通过维护状态机来读写请求
  • RPC

多机房数据一致性

微服务混合云部署实践

跨云服务的负载均衡

当服务上云后还需要考虑把一定比例的用户请求路由到云上部署的服务

跨云服务的数据同步

私有云与公有云之间的网络隔离

一般来讲,出于安全的需要,企业内部机房同公有云机房之间的网络是隔离的,为了实现互通,需要架设专门的 VPN 网络或者专线。

数据库能否上云

数据库能否上云的关键取决于数据的隐私性。

跨云服务的容器运维

跨云的主机管理:跨云主机管理的关键点在于,如何对内部私有云的机器和公有云的 ECS 进行管理,

跨云服务发现

跨云弹性扩容

跨云服务编排

下一代微服务架构 Service Mesh

为什么需要 Service Mesh

  • 跨语言服务调用的需要

  • 云原生应用服务治理的需要

Service Mesh 的实现原理

Service Mesh 实现的关键点:

  • 轻量级网络代理 SideCar,它的作用就是转发服务之间的调用;
  • 基于 SideCar 的服务治理也被叫作 Control Plane,它的作用是向 SideCar 发送各种指令,以完成各种服务治理功能。
  • 服务发现
  • 负载均衡
  • 请求路由
  • 故障处理
  • 安全认证
  • 监控上报
  • 日志记录
  • 配额控制

Istio:Service Mesh 的代表产品

Istio 整体架构

Istio 的架构可以说由两部分组成,分别是 Proxy 和 Control Plane。

  • Proxy,就是前面提到的 SideCar,与应用程序部署在同一个主机上,应用程序之间的调用都通过 Proxy 来转发,目前支持 HTTP/1.1、HTTP/2、gRPC 以及 TCP 请求。
  • Control Plane,与 Proxy 通信,来实现各种服务治理功能,包括三个基本组件:Pilot、Mixer 以及 Citadel。

《极客时间教程 - 左耳听风》笔记

洞悉技术的本质

分布式系统架构的本质

分布式系统架构的优点:

  • 高性能
  • 高可用

分布式系统架构的缺点:

  • 设计复杂
  • 运维复杂

分布式系统的技术栈

提高性能的技术

  • 缓存
  • 负载均衡
  • 异步
  • 分片

提供可用性的技术

  • 服务拆分
  • 服务冗余
  • 流量控制
  • 高可用架构:多租户、多活架构、灾备
  • 高可用运维:监控、DevOps

分布式系统的关键技术

  • 服务治理
  • 服务、资源调度
  • DevOps
  • 监控

编程范式游记

分布式系统设计模式

区块链

程序员练级攻略

面试攻略

高效学习

浅度学习和深度学习

  • 高质量的信息源和第一手的知识
  • 把知识连成地图,将自己的理解反述出来
  • 不断地反思和思辨,与不同年龄段的人讨论
  • 举一反三,并践行之,把知识转换成技能

深度,归纳和坚持实践

  1. 这个技术出现的背景、初衷和目标
  2. 这个技术的优势和劣势分别是什么
  3. 这个技术适用的场景
  4. 技术的组成部分和关键点
  5. 技术的底层原理和关键实现
  6. 已有的实现和它之间的对比

高效沟通

电商

基本业务架构

img

订单

订单服务一般不主动调用其他服务

订单服务不负责和第三方集成

订单服务不提供优惠计算或成本分摊逻辑

订单信息管理

  • 用户
  • 商品
  • 收货人
  • 收货地址
  • 收货时间
  • 订单状态

优惠券

典型问题

秒杀活动

超卖