Neo's Blog

不抽象就无法深入思考
不还原就看不到本来面目!

0%

在一个二维数组中,每一行都按照从左到右递增的顺序排序,每一列都按照从上到下递增的顺序排序。

请完成一个函数,输入这样的一个二维数组和一个整数,判断数组中是否含有该整数。

样例
输入数组:

[
[1,2,8,9],
[2,4,9,12],
[4,7,10,13],
[6,8,11,15]
]

如果输入查找数值为7,则返回true,

如果输入查找数值为5,则返回false。

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
public:
bool searchArray(vector<vector<int>> array, int target) {
if (array.empty() || array[0].empty()) return false;
int i = 0, j = array[0].size() - 1;
while (i < array.size() && j >= 0) {
if (array[i][j] == target) return true;
if (array[i][j] > target) j--;
else i++;
}
return false;
}
};

关于二分

二分是分治的一种;问题收敛速度最快的一种。

通过每次把问题搜索空间排除一半,进而得到lgN的时间复杂度。

将区间分为两部分,第一部分满足某一个特征;第二部分满足另一个特征

通过二分来解决的问题包括:

  1. 二分查找一个数组
  2. 通过二分的方式,遍历所有可能的结果。然后判定结果是否ok。也就是把查找问题转换为判定问题。

二分答案:答案具有“单调性”,外层花费$logN$的时间转化为判定性问题.

答案具有“单调性”:注意是先0后1函数,还是先1后0函数,和二分的实现形式,是一直保持小于,还是能累加就累加?

一般定义判定是“是否存在一个小于等于、是否存在一个大于等于”,这么定义是显然有单调性的

二分出的答案一般对check的进行有所帮助

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
//整数二分算法模板 https://www.cnblogs.com/smallocean/p/11913963.html

bool check(int x) {return true;/* ... */} // 检查x是否满足某种性质

// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用:
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid; // check()判断mid是否满足性质
else l = mid + 1;
}
return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用:
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}

//浮点数二分算法模板 —— 模板题 AcWing 790. 数的三次方根
bool check(double x) {/* ... */} // 检查x是否满足某种性质

double bsearch_3(double l, double r)
{
const double eps = 1e-6; // eps 表示精度,取决于题目对精度的要求
while (r - l > eps)
{
double mid = (l + r) / 2;
if (check(mid)) r = mid;
else l = mid;
}
return l;
}

二分相关题目索引

题目分类 题目名称 考察点 其他说明
查第一个出现的位置
递增找number与index相等
重复数字出现次数
旋转数组找最值
求两个有序数组前K大的数 考虑A的中间元素m/2和B的中间元素n/2
多路归并 求m个有序数组前K大的数 维护一个大小为m的堆

参考:

https://zhuanlan.zhihu.com/p/157779732

https://www.dazhuanlan.com/2019/12/16/5df6e82127d88/

https://blog.csdn.net/weixin_34355715/article/details/94465888

https://blog.csdn.net/weixin_43626741/article/details/104364387

分布式锁应该具备哪些条件

  1. 在分布式系统环境下,一个方法在同一时间只能被一个机器的一个线程执行
  2. 高可用的获取锁与释放锁
  3. 高性能的获取锁与释放锁
  4. 具备可重入特性(可理解为重新进入,由多于一个任务并发使用,而不必担心数据错误)
  5. 具备锁失效机制,防止死锁
  6. 具备非阻塞锁特性,即没有获取到锁将直接返回获取锁失败

ps. 考虑幂等性

分布式锁实现

基于数据库

基于Redis SET命令

基于Memcache CAS命令

基于Zookeeper

具体参考

https://www.jianshu.com/p/a1ebab8ce78a

设计目标:

  1. 高性能,更快的找到下一个超时的Task
  2. 高分辨率,例如毫秒定时器

实现方式:

  • 时间轮、多级时间轮
  • 基于排序链表 O(N)
  • 红黑树

并发 = rt * qps

N = X * R

N表示系统中同时活动的用户,包括正在处理中和队列中的;X表示用户相继到达系统的速率,在平衡状态时即为系统吞吐量(到达=离开);R表示每个用户在系统中平均的驻留时间

eg. 一个请求在系统中的停留时间,1s; 一秒钟平均过来100个请求(当然一秒钟也会离开100个请求)那么系统同时处理的请求数是多少?

应该是1s * 100 (个/s) = 100个。

Eric的估算公式 C = L * n / T

n表示同时在线;L表示平均停留时间;T表示高峰期持续时间;C为并发用户数量。

eg. 一个用户在系统中的停留时间,30分钟;晚高峰5~7点,有500在线;问系统的并发用户是多少?

假设系统后端维护session,那么这个session的长度即为30分钟,L=30minutes;用户活跃时长已经得知是从5点到7点共2个小时,T=2 hours;那么并发用户数C=nL/T=50030/(5*60)=50

等价性说明

我们再来从另外的角度分解Eric的估算公式:

C=nL/T 可以表示为 C=(n/T)L

n/T 是不是和我们刚才在上面Little中第一步一样,是计算到达率X的。

而L(session时长)不就是R(驻留时间)吗?都等同于session时间长度。

也就是说C=(n/T)L=XR=N

结论:由以上得知,Eric’s 估算公式跟Little定律是等价的.

参考:https://www.cnblogs.com/hundredsofyears/p/3360305.html

并发 = 到达率X * 停留时间R

并发 = (同时在线人数c / 高峰期持续时长T) * 平均停留时间L

服务的处理能力是有客观上限的。当请求速度超过服务的处理速度时,服务就会过载。

如果服务持续过载,会导致越来越多的请求积压,最终所有的请求都必须等待较长时间才能被处理,从而使整个服务处于瘫痪状态。

与之相对的,如果直接拒绝掉一部分请求,反而能够让服务能够”及时”处理更多的请求。 我们称这种做法为限流。限流是过载保护的手段之一,目的在于保护系统不被超过服务吞吐能力的流量(或突发)打死。

常见的限流手段有固定窗口、滑动窗口、令牌桶、漏桶、BBR、brpc自适应限流等。

下面挨个介绍每一种限流算法的原理以及优缺点。

传统的基于配置的单机限流算法

第一,固定窗口

原理:维护一个窗口的两个信息:窗口开始时间、经过窗口的请求数。如果窗口内超出限制数则拒绝。如果已经进入新的窗口,则reset计数。

不足:存在临界点(窗口reset前后的瞬时)的qps会飙高,从而打死系统。

第二,滑动窗口

原理:将总的时间窗口划分为N个小格,并单独维护每一个小格的计数。通过这种方式,可以把过去一段时间内的计数信息也用上,从而让限流的统计更精确一些。

不足:窗口不可能无限划分

第三,漏桶

原理:漏桶具有固定容量,出水速率是固定常量(流出请求)如果桶是空的,则不需流出水滴。可以以任意速率流入水滴到漏桶(流入请求)如果流入水滴超出了桶的容量,则流入的水滴溢出(新请求被拒绝)

不足:无法应对突发流量

20201222223148

第四,令牌桶

原理:假如用户配置的平均发送速率为r,则每隔1/r秒一个令牌被加入到桶中。假设桶最多可以存发b个令牌。如果令牌到达时令牌桶已经满了,那么这个令牌会被丢弃。当一个n个字节的数据包到达时,就从令牌桶中删除n个令牌,并且数据包被发送到网络。如果令牌桶中少于n个令牌,那么不会删除令牌,并且认为这个数据包在流量限制之外。

算法允许最长b个字节的突发,但从长期运行结果看,数据包的速率被限制成常量r。

20201222223820

自适应限流

上面的几种算法,一定程度上确实能够保护系统不被拖垮, 但不管漏斗桶还是令牌桶, 其防护思路都是设定一个指标, 当超过该指标后就阻止或减少流量的继续进入,当系统负载降低到某一水平后则恢复流量的进入。但其通常都是被动的,其实际效果取决于限流阈值设置是否合理,但往往设置合理不是一件容易的事情。具体有如下几个弊端:

  1. 集群增加机器或者减少机器限流阈值是否要重新设置?
  2. 设置限流阈值的依据是什么?
  3. 人力运维成本是否过高?
  4. 只针对局部服务端的限流,无法掌控全局资源,
  5. 当调用方反馈限流错误时, 这个时候重新设置限流, 其实流量高峰已经过了。重新评估限流是否有意义?

第五,B站BBR限流

底层原理:并发 = qps * latency

自适应限流能动态调整服务的最大并发,在保证服务不过载的前提下,让服务尽可能多的处理请求。

具体逻辑:如果cpu > 800 && inflight > rt * max_qps,则启用限流

为什么要用 CPU\IOPS 作为启发值呢?

因为自适应限流与 TCP 拥塞控制还存在不同之处,TCP 中客户端可以控制发送率,从而探测到 maxPass,但是 RPC 线上无法控制流量的速率,所以必须以 CPU 作为标准,当 CPU 快满载的时候再开启,这时我们认为之前探测到的 maxPass 已经接近了系统的瓶颈,乘以 minRtt 就可以得到 InFlight

第六,BRPC自适应限流

  • peek_qps 峰值qps
  • nodelay_latency 系统无负载或者低负载的响应时间,理论值无法计算
  • Min_latency 用来替代nodelay_latency
  • max_cocurrent = max_qps * ((2+alpha) * min_latency - latency)

服务的noload_latency并非是一成不变的,自适应限流必须能够正确的探测noload_latency的变化。当noload_latency下降时,是很容感知到的,因为这个时候latency也会下降。难点在于当latency上涨时,需要能够正确的辨别到底是服务过载了,还是noload_latency上涨了。

参考:

BRPC开发手册

B站在微服务治理中如何探索与实践

设计思想

cosume group

通过cosume group,巧妙的解决了广播问题。

高性能

SendFile、zero copy

索引设计

Kafka为了卸载MQ本身的复杂性,为了其真正无状态的设计,它将状态维护机制这口锅完全甩给了消费者,因此取消息的问题就转化成了消费者拿着一个offset索引来Kafka存储器里取消息的问题,这就涉及到了性能。But 如何能查的更快?How?

扫描partition文件

但实际上,

partition

  1. partition并不是一根文件,而是一个目录
  2. 目录下面存了很多逻辑上的segment,每一个segment物理上包括两个文件:索引文件、日志文件(每次都append)

文件的命名相当于查找的稀疏索引,省去索引文件

每个segment索引又是一个稀疏索引减少索引文件的大小

在 Kafka 中,生产者写入消息、消费者读取消息的操作都是与 leader 副本进行交互的,从 而实现的是一种主写主读的生产消费模型。

16.为什么Kafka不支持读写分离?

Kafka 并不支持主写从读,因为主写从读有 2 个很明 显的缺点:

(1)数据一致性问题。数据从主节点转到从节点必然会有一个延时的时间窗口,这个时间 窗口会导致主从节点之间的数据不一致。某一时刻,在主节点和从节点中 A 数据的值都为 X, 之后将主节点中 A 的值修改为 Y,那么在这个变更通知到从节点之前,应用读取从节点中的 A 数据的值并不为最新的 Y,由此便产生了数据不一致的问题。

(2)延时问题。类似 Redis 这种组件,数据从写入主节点到同步至从节点中的过程需要经 历网络→主节点内存→网络→从节点内存这几个阶段,整个过程会耗费一定的时间。而在 Kafka 中,主从同步会比 Redis 更加耗时,它需要经历网络→主节点内存→主节点磁盘→网络→从节 点内存→从节点磁盘这几个阶段。对延时敏感的应用而言,主写从读的功能并不太适用。

replication

2.1 消息备份

Kafka允许同个Partition存在多个消息副本(Replica),每个Partition的副本通常由1个Leader及0个以上的Follower组成,产者将消息直接发往对应Partition的Leader,Follower会周期地向Leader发送同步请求,Kafka的Leader机制在保障数据致性地同时降低了消息备份的复杂度。同Partition的Replica应存储在同一个Broker上,因为一旦该Broker宕机,对应Partition的所有Replica都无法作,这就达不到高可用的效果。为做好负载均衡并提容错能,Kafka会尽将所有的Partition以及各Partition的副本均匀地分配到整个集群上。举个例,当集群中部署3台Broker,TopicA共有4个Partition,每个Partition均有3个Replica时下图就是种合理的分布方式。

ISR:In-Sync Replicas 副本同步队列

ISR(In-Sync Replicas)指的是个Partition中与Leader“保持同步”的Replica列表(实际存储的是副本所在Broker的BrokerId),这的 保持同步是指与Leader数据保持完全一致,只需在replica.lag.time.max.ms时间内与Leader保持有效连接,官方解释如下If a follower hasn’t sent any fetch requests or hasn’t consumed up to the leaders log end offset for at least this time, the leader will remove the follower from isr,( default value =10000 )Follower周期性地向Leader发送FetchRequest请求(数据结构见下),发送时间间隔配置在replica.fetch.wait.max.ms中,默认值为 500。

AR:Assigned Replicas 所有副本

ISR是由leader维护,follower从leader同步数据有一些延迟(包括延迟时间replica.lag.time.max.ms和延迟条数replica.lag.max.messages两个维度, 当前最新的版本0.10.x中只支持replica.lag.time.max.ms这个维度),任意一个超过阈值都会把follower剔除出ISR, 存入OSR(Outof-Sync Replicas)列表,新加入的follower也会先存放在OSR中。AR=ISR+OSR。

ISR数据同步、ACK选项(全部ack、只有一个ack)

Kafka的复制机制既不是完全的同步复制,也不是单纯的异步复制。

完全同步复制要求All Alive Follower都复制完,这条消息才会被认为commit,这种复制方式极大的影响了吞吐率。

而异步复制方式下,Follower异步的从Leader复制数据,数据只要被Leader写入log就被认为已经commit,这种情况下,如果leader挂掉,会丢失数据。

kafka使用ISR的方式很好的均衡了确保数据不丢失以及吞吐率。Follower可以批量的从Leader复制数据,而且Leader充分利用磁盘顺序读以及send file(zero copy)机制,这样极大的提高复制性能,内部批量写磁盘,大幅减少了Follower与Leader的消息量差。

leader会维护一个与其基本保持同步的Replica列表,该列表称为ISR(in-sync Replica),每个Partition都会有一个ISR,而且是由leader动态维护 ,如果一个follower比一个leader落后太多,或者超过一定时间未发起数据复制请求,则leader将其重ISR中移除

Log End Offset

当前broker收到的最新offset

HighWatermark

已经同步到其他slave中的offset

Leader epoch

由于follower的HW的更新,需要一轮额外的消息拉取,如果folloer很多的话,就需要多轮拉取Leader 副本高水位更新和 Follower 副本高水位更新在时间上是存在错配的,会导致数据的
不一致,所以Leader epoch登场。

Epoch,一个单调增加的版本号。每当leader发生变更时,都会增加该版本号。小版本号的Leader 被认为是过期 Leader,不能再行使 Leader权力。

起始位移,Leader 副本在该 Epoch 值上写入的首条消息的位移类似于zookeper的leader机制,通过leader epoch的单调递增,以此避免副本宕机重启导致的消息同步错乱

参考:

https://blog.csdn.net/dog250/article/details/79588437
https://www.jianshu.com/p/469ec6dcdc02
https://blog.csdn.net/qq_28900249/article/details/90346599

可能的一些作用:

  1. 非核心逻辑异步化,追求高性能
  2. 解除耦合,Event Driven 事件驱动设计
  3. 实现广播
  4. 削峰填谷,把峰值流量缓冲下来,后面慢慢处理

具体可以用于:

  1. 分布式事务,单方生产,多个消费业务逻辑
  2. 数据复制:通过消息队列,将数据复制到多个目的地(多维度数据表、ES、Hadoop、搜索等)
  3. 日志同步:多个app生产日志并放入队列,然后消费队列完成日志的离线与实时处理
  4. 延迟队列:可靠的延迟队列,分布式环境定时器
  5. 广播通知:Cache失效通知

消息队列的缺点

系统可用性降低:系统引入的外部依赖越多,越容易挂掉,本来你就是A系统调用BCD三个系统的接口就好了,人ABCD四个系统好好的,没啥问题,你偏加个MQ进来,万一MQ挂了咋整?MQ挂了,整套系统崩溃了,你不就完了么。

系统复杂性提高:硬生生加个MQ进来,你怎么保证消息没有重复消费?怎么处理消息丢失的情况?怎么保证消息传递的顺序性?头大头大,问题一大堆,痛苦不已

一致性问题:A系统处理完了直接返回成功了,人都以为你这个请求就成功了;但是问题是,要是BCD三个系统那里,BD两个系统写库成功了,结果C系统写库失败了,咋整?你这数据就不一致了。

主流消息队列对比

特性 ActiveMQ RabbitMQ RocketMQ kafka
单机吞吐量 万级,吞吐量比RocketMQ和Kafka要低了1个数量级 同ActiveMQ 10万级,可支撑高吞吐 10w量级
topic数量对吞吐量的影响 topic可达几百几千,吞吐小幅下降,一大优势 topic从几十到上百,吞吐大幅下降;单机支持topic不宜过多,如有需要可以加更多机器
MasterSlave 主-从 主-从 物理Master-Slave 逻辑上Master-Slave,按照Partition
分布式消息事务 支持 支持 不支持
延迟消息 支持 不支持
消息投递实时性 RocketMQ使用长轮询,同Push方式实时性一致,消息的投递延时通常在几个毫秒 Kafka使用短轮询方式,实时性取决于轮询间隔时间
消费失败重试 RocketMQ消费失败支持定时重试,每次重试间隔时间顺延 Kafka消费失败不支持重试
消息顺序 RocketMQ支持严格的消息顺序,在顺序消息场景下,一台Broker宕机后,发送消息会失败,但是不会乱序 支持 Kafka支持消息顺序,但是一台Broker宕机后,就会产生消息乱序
主从选举 不需要选举NameServer KRaft选举
时效性 ms us,最大特点,延迟低 ms ms
可用性 高,基于主从架构实现高可用性 同ActiveMQ 非常高,分布式架构 非常高,分布式,单数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用
消息可靠性 较低概率丢数据 经过参数优化配置,可以做到0丢失 同RocketMQ
功能支持 极其完备 基于erlang开发,所以并发能力很强,性能极其好,延时很低 MQ功能较为元善,还是分布式的,扩展性好 功能较为简单
总体优劣势 早期使用,社区不活跃 吞吐低,语言不熟,开源社区活跃,小公司 阿里品牌保障(利弊)要求技术研发力量,大公司 大数据领域

Master/Slave概念差异
Kafka:Master/Slave 是个逻辑概念,1 台机器同时是 Master 和 Slave。
RocketMQ:Master/Slave 是个物理概念,1 台机器只能是 Master 或者 Slave。

参考:https://blog.csdn.net/wdj_yyds/article/details/123534023

分布式消息队列评价指标

可靠

分布式消息队列提供更好的可靠性,主要体现在:

  1. 消息会被持久化到分布式存储中。这样避免了单台机器存储的消息由于机器问题导致消息的丢失;
  2. 不佳的网络环境中,保证只有当消息的接收者确实收到消息时才从队列中删除消息。

可扩展

可扩展性体现在访问量和数据量两个方面:

访问量:分布式消息队列服务,会随着访问量的增减而自动增减逻辑处理服务器;

数据量:当数据量扩大时,后端分布式存储会自动扩容。

安全

安全体现在以下两个方面:

  1. 同时使用消息队列的业务之间不会互相干扰。如果有多个业务同时在使用消息队列,对于单机的消息队列服务,一个业务的消息操作可能会影响其他业务的正常运行。比如,一个业务的消息操作特别频繁,占据了消息队列的绝大部分服务时间,也占据了这台服务器的绝大部分网络IO,导致其他业务无法正常地与消息队列通信。而且甚至可能由于服务控制不当,导致机器崩溃,服务停止,业务也跟着停止。分布式消息队列则不会出现这个问题:
    (1)监控措施完善,系统性能指数会控制在一定范围之内,而且有任何异常也会报警;
    (2)当访问量和数据量增大时,分布式消息队列服务可以自动扩展。

  2. 各业务的消息内容是安全存储的,其他业务不能访问到非自身业务的数据。
    一方面是业务需要密钥来访问消息队列;另一方面,消息是被加密存储的。

简单实用

简单实用体现在:

  1. 透明:接收者和发送者无需知道具体的消息队列的服务器地址,服务器的增减对接收者和发送者透明。
  2. 实用:对于两个服务之间不能通信的网络情况,消息队列为他们提供了恰到好处的桥梁。

如何保证消息不丢

生产端不丢消息

生产端如何保证不丢消息呢?确保生产的消息能到达存储端。

如果是RocketMQ消息中间件,Producer生产者提供了三种发送消息的方式,分别是:

  • 同步发送
  • 异步发送
  • 单向发送

生产者要想发消息时保证消息不丢失,可以:

  • 采用同步方式发送,send消息方法返回成功状态,就表示消息正常到达了存储端Broker。
  • 如果send消息异常或者返回非成功状态,可以重试。
  • 可以使用事务消息,RocketMQ的事务消息机制就是为了保证零丢失来设计的

存储端不丢消息

如何保证存储端的消息不丢失呢? 确保消息持久化到磁盘。大家很容易想到就是刷盘机制。

刷盘机制分同步刷盘和异步刷盘:
生产者消息发过来时,只有持久化到磁盘,RocketMQ的存储端Broker才返回一个成功的ACK响应,这就是同步刷盘。它保证消息不丢失,但是影响了性能。
异步刷盘的话,只要消息写入PageCache缓存,就返回一个成功的ACK响应。这样提高了MQ的性能,但是如果这时候机器断电了,就会丢失消息。

Broker一般是集群部署的,有master主节点和slave从节点。消息到Broker存储端,只有主节点和从节点都写入成功,才反馈成功的ack给生产者。这就是同步复制,它保证了消息不丢失,但是降低了系统的吞吐量。与之对应的就是异步复制,只要消息写入主节点成功,就返回成功的ack,它速度快,但是会有性能问题。

消费阶段不丢消息

如何保证消息顺序

消费者执行完业务逻辑,再反馈会Broker说消费成功,这样才可以保证消费阶段不丢消息。

消息队列保证顺序性整体思路就是这样啦。比如Kafka的全局有序消息,就是这种思想的体现: 就是生产者发消息时,1个Topic只能对应1个Partition,一个 Consumer,内部单线程消费。

但是这样吞吐量太低,一般保证消息局部有序即可。在发消息的时候指定Partition Key,Kafka对其进行Hash计算,根据计算结果决定放入哪个Partition。这样Partition Key相同的消息会放在同一个Partition。然后多消费者单线程消费指定的Partition。

如何实现消息事务

下面是RabbitMQ/RocketMQ消息事务的实现机制

image

  1. 生产者产生消息,发送一条半事务消息到MQ服务器
  2. MQ收到消息后,将消息持久化到存储系统,这条消息的状态是待发送状态。
  3. MQ服务器返回ACK确认到生产者,此时MQ不会触发消息推送事件
  4. 生产者执行本地事务
  5. 如果本地事务执行成功,即commit执行结果到MQ服务器;如果执行失败,发送rollback。
  6. 如果是正常的commit,MQ服务器更新消息状态为可发送;如果是rollback,即删除消息。
  7. 如果消息状态更新为可发送,则MQ服务器会push消息给消费者。消费者消费完就回ACK。
  8. 如果MQ服务器长时间没有收到生产者的commit或者rollback,它会反查生产者,然后根据查询到的结果执行最终状态。

集成:在服务提供端或者调用端,如何集成注册中心?应用内 OR 应用外

测活:服务注册之后,如何对服务进行测活以保证服务的可用性

负载均衡:当存在多个服务提供者时,如何均衡各个提供者的负载?

运行时依赖:引入注册中心之后,对应用的运行时环境有何影响?

可用性:如何保证注册中心本身的可用性,特别是消除单点故障?

状态获取:(1)主动探测(2)心跳上报

一些可能问题:

  • 保护机制:如果短时间内摘除的节点数量超过集群的40%,则停止摘除节点
  • 通知风暴问题(准备更多的注册中心节点;精简通知内容)

目前比较流行的解决方案包括:

对比维度 euraka consul ZK
实现思路 去中心化,通过复制来同步数据,但不保证一致性。只要有一个节点可用,系统整体就可用。 内置了服务注册与发现框架、分布一致性协议实现、健康检查、Key/Value 存储、多数据中心方案 使用ZK节点临时节点来维护服务器列表,ZK支持watch节点变更通知机制
测活 客户端心跳 TCP/HTTP/gRPC/Cmd 自研-客户端心跳
负载均衡 Ribbon Fabio 自研-负载均衡
雪崩保护 有,灾难态进入自我保护模式 自研
自动注销实例 支持 支持 进程不可用,临时节点自动销毁
访问协议 HTTP HTTP/DNS 自研
监听支持 支持 支持 WATCH NODE变更
多数据中心 支持 支持 不支持
跨注册中心同步 支持 支持 不支持
框架集成 SpingCloud继承 Spring、k8s集成
优点 去中心化,高可用 功能相对完善 简单容易实现,适用于初创期
不足 一致性差 可用性无保证 服务可用性无保障
ZK跨机房集群支持不佳
CAP AP CP CP
一致性协议 仅复制,非强一致 raft zab,一种类paxos协议

20201222213947

单体到微服务

为什么要转向微服务架构(收益)

  1. 系统扩展性不足(例如,因为DB连接问题导致无法持续扩容)
  2. 迭代效率低。如果是预估到业务在飞速增长,那就别犹豫,一定要提前考虑微服务的拆分。
  3. 系统编译、部署成本高
  4. 如果在设计架构的时候,发现需要很多异构的技术栈,那也要考虑下微服务。

转向微服务架构需要克服什么困难(成本)

  1. 技术基础设施要求比较高(如果公司技术基础设施非常完备,对应的业务起初就设计的非常复杂,那么也别犹豫,起手就上微服务。)
  2. 工程拆分挑战比较大,实现时容易为了拆分而拆分。

拆分原则

从单体到微服务,也许可以有一个过度:工程的拆分

微服务拆分的大原则

  1. 单一服务内的高内聚、低耦合,单一职责
  2. 拆分粒度:先粗拆,后细拆(伴随业务复杂度的变多,或者对业务的理解程度加深)
  3. 拆分过程中,要避免影响业务的日常功能迭代—按照依赖顺序,挨个拆分
  4. 确保服务接口定义有可扩展性
  5. 避免环形依赖问题(通过分层,例如业务层、数据访问层)

业务优先,组织结构,质量维度

SRP Single Responsibilty 单一指责原则
CCP Common Closure Principle 共同闭包原则,包中包含的所有类应该是同类的变化的一个集合,也就是说,如果对包作出修改,需要调整的类应该都在这个包之内。

你的团队成员结构是什么样的,你的架构就会长成啥样。

团队按照业务边界来拆分;确保团队不要太大

因为微服务就是为了减少研发成本,而包括沟通成本。

服务拆分带来的问题

  1. 接口调用耗时增加
  2. 如何知道调用哪个服务?服务注册中心
  3. 服务治理体系:熔断、限流、降级、超时控制等
  4. 问题排查困难(分布式链路追踪)

微服务组件

API网关

入口网关:隔离客户端与微服务,协议转换、安全策略、认证、限流、熔断等

出口网关:调用外部API,统一认证,授权、授权、访问控制等

性能:IO多路复用、异步非阻塞、线程池

设计要点:性能、扩展性(责任链模式)

隔离性:针对接口对线程池进行分类

服务注册与发现

zookeeper, consul, euraker

负载均衡

RoundRobin, Hash, Weight

熔断、限流
配置实时下发
客户端

配置项的读取-变更推送如何实现:

  1. 轮询 + 摘要 (简单,实时性略差)
  2. 长轮询 + 版本号(复杂,实时性好一些)

client的高性能实现逻辑:

  1. DoubleBufferedData, 数据分前台和后台
  2. 读拿到自己所在线程的thread-local读写锁,执行查询逻辑后释放锁。
  3. 同时只有一个写:修改后台数据,切换前后台,挨个获得所有thread-local锁并立刻释放,结束后再改一遍新后台(老前台)。

一个小原则:配置系统的旁路化,不要因为配置系统挂了,你的程序启动不了;
可以做两层缓存:内存缓存、文件保存

服务端

不同的配置类型:节点类型 》机房类型 》 全局配置

配置项的存储-配置系统的高可用

核心指标在于可用性!!!5个9?

对接调用链追踪 (Jager)

trace_id + span_id来标识链路的调用关系。

对trace_id采样,而不要随机采样。

span 基本工作单元,一次链路调用(可以是RPC,DB等没有特定的限制)创建一个span,通过一个64位ID标识它,uuid较为方便,span中还有其他的数据,例如描述信息,时间戳,key-value对的(Annotation)tag信息,parent_id(基于parent_span_id来维护树形结构)等,其中parent-id可以表示span调用链路来源。

trace_id 类似于 树结构的Span集合,表示一次完整的跟踪,从请求到服务器开始,服务器返回response结束,跟踪每次rpc调用的耗时,存在唯一标识trace_id。比如:你运行的分布式大数据存储一次Trace就由你的一次请求组成。

Annotation 注解,用来记录请求特定事件相关信息(例如时间),一个span中会有多个annotation注解描述。通常包含四个注解信息:

监控系统

监控哪些内容

监控系统架构

方案:普罗米修斯、Graphna

APM:端到端的监控体系

如何防止消息被篡改:对消息体 + 消息头 进行加密,生成一个签名
如何对数据进行加密:

使用非对称加密的公钥对 “对称加密的私钥-OriginPrivate“ 进行加密,得到SecretPrivate
然后服务端利用非对称加密的私钥,对 SecretPrivate进行解密,得到OriginPrivate

然后再使用OriginPrivate对加密之后的消息体(SecretContext)进行解密,得到Contenxt

监控哪些东西:网络卡顿率、做某件事情的失败率等

考虑暂存 + retry来应对网络状况不佳的情况

自动化全链路压测系统

压测的原则-尽量模拟真实情况;压测的注意点:
(1)使用线上数据与线上数据
(2)使用线上流量(流量拷贝)
(3)流量应该从尽量靠近用户的CDN发起

如何搭建:
(1)流量的隔离(区分压测流量与正式流量)
(2)风险控制(尽量避免压测对正常用户的影响)

自动化全链路压测系统

压测数据的产生:
拷贝真实流量(可以从访问日志、可以抓取某个端口的数据等)
打上压测标签
放在合适的机房(尽量接近用户)
数据隔离:
针对读请求,针对某些不能压测(例如推荐、数据分析等)的组件进行mock
对于写请求,把流量产生的数据写入影子库(数据库-拷贝一份库表和数据;缓存-加压测前缀;ES-多搞一份索引)
压力测试的实施
持续放大,做好系统过载的识别(例如超时率、resp time等)