前言

记一次代码检视中领悟到的知识,和大家一起交流

正文

提交上来的代码大概是这个样子的

1
2
3
4
5
Socket socket = new Socket(ip, port);
final DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
dataOutputStream.write("HelloWorld".getBytes(StandardCharsets.UTF_8));
socket.shutdownOutput();
dataOutputStream.flush();

这次主要是添加shutdownOutput的调用,及时关闭tcp会话,防止TW过多。

经过大家的讨论,主要的矛盾点在shutdownOutputflush的顺序。首先想到的是 flush方法放在了output后面,这样还能起作用吗?但是提交代码之前是经过测试的,这样子是可以正常工作的。然后的想法就是,傻逼了,想错了,shutdown应该自带flush效果,os都发fin了,之前的buffer肯定出去了。

我做个实验,来探究下是不是这样子的,我从python开启了一个http server来开启实验

1
python3 -m http.server

java测试类代码

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
package com.github.hezhangjian.demo.basic;

import lombok.extern.slf4j.Slf4j;
import org.junit.Test;

import java.io.DataOutputStream;
import java.net.Socket;
import java.nio.charset.StandardCharsets;

/**
* @author hezhangjian
*/
@Slf4j
public class DemoSocketSend {

String ip = "127.0.0.1";

int port = 8000;

@Test
public void testSocketSend() throws Exception {
Socket socket = new Socket(ip, port);
final DataOutputStream dataOutputStream = new DataOutputStream(socket.getOutputStream());
dataOutputStream.write("HelloWorld".getBytes(StandardCharsets.UTF_8));
socket.shutdownOutput();
dataOutputStream.flush();
}

}

随后我在25行和26行打了断点

当运行到25行的时候,python server并没有收到数据

image-20210402121759024

还没运行26行的时候,数据就已经发送到python服务器了

image-20210402121818000

总结

这个时候证明我们的推测是正确的,shutdownOutput方法自带了flush效果。
我也尝试了配置tcpNoDeplay参数,配不配置tcpNoDelay,都是一样的效果。看起来jvm都有缓冲
那么已经调用了shutdownOutput方法之后,flush方法还有没有必要调用呢,从clean code的角度,flush方法的调用已经是没有任何必要的了,建议删除。一般场景下可能不会有问题,但是如果极端场景,比如在25行到26行之间,程序陷入了长gc,这行就有可能抛出IOException,影响原来的逻辑。

背景

我们的业务有些时候总是在升级期间rpc业务有一些呼损,想总结一下让rpc调用零呼损的两种方式:重试和优雅启停。我先介绍这两种方式,再描述一下这两种方式的优缺点

rpc-lossless

A是一个微服务

B也是一个微服务

蓝色的是常见的注册中心,有zookeepereureka等实现。

重试

重试,在发生可重试错误的时候,重试一次。什么是可重试错误呢?就是重试一次,可能会成功。比如400 BadRequest,那出现这种错误,基本上重试也没有用,就不要浪费我们宝贵的服务器资源了。常见的如servicecomb框架就有重试几次、重试间隔这样的参数。值得一提的是,如果你指望通过重试让升级零呼损,那么你的重试次数,要比你的并行升级实例数大才行。

这也很容易理解,比如A服务调用B服务,B服务有5个实例,B1~B5。这个时候,同时升级B1和B2,A第一次调用了B1,接下来重试,如果运气不好,恰好重试到了B2节点,那么业务还是会失败的。如果防异常故障,就得重试三次才行。

如果是防止单数据中心宕机,重试次数大于同时宕机节点数,这个规则可能就没那么靠谱了。现在,企业部署十几个乃至二十几个微服务实例,已经不是什么新闻了,假设分3数据中心部署,总不能重试接近10次吧,这种时候,最好重试策略和数据中心相关,重试的时候,选择另一个az的实例。目前servicecomb还不支持这种功能。

优雅启停

优雅停止

优雅停止,就是说当微服务快要宕机的时候,先从注册中心进行去注册,然后把发送给微服务的消息,处理完毕后,再彻底关闭。这个方式,可以有效地防止升级期间,发送到老节点的呼损。

优雅启动

优雅启动,当微服务实例,能够处理rpc请求的时候,再将实例自己注册到注册中心。避免请求发进来,实例却无法处理。

这里有一个要求,就是调用方发现被调用方(即A发现B)的注册中心,要和B注册、去注册的注册中心是一个注册中心。有案例是,发现采用k8s发现,注册、去注册却使用微服务引擎,导致呼损。

优劣对比

可预知节点升级的场景

重试相对于优雅启停,在预知节点升级的场景没那么优雅,重试次数可能还要和并行升级的节点挂钩,非常的不优雅,且难以维护

不可预知节点升级的场景

优雅启停无法对不可预知节点升级的场景生效。只有重试能在这个场景发挥作用

其他场景

重试可以很好地处理网络闪断、长链接中断等场景

总结

想要实现rpc调用零呼损,重试和优雅启停都不可或缺,都需要实现。

Dapper出现的背景

分布式系统不容易观测。有些问题靠日志和统计根本无法挖掘。
有些无法重现或极难重现的场景。

Dapper设计的原则

低时延

微不足道的性能影响,使应用程序团队愿意迁移。

应用透明

应用尽量少做侵入式修改

可扩展

随着应用程序的规模扩展

Dapper概念

概览

image-20210321221020253

image-20210321214734150

通过引入parent id和span id等来将调用链串起来

trace id

特定的模式是trace id

span id

span,包括日志,起始、终止时间,也包括key、value。还区分了网络和非网络时延。

annotation

应用程序根据自己的需要可以打上annotation,不仅仅是key、value,还可以有时间戳等,有助于分析方法级别的耗时

image-20210321221204695

Dapper原理

  • 每当一个线程处理一个采样的控制线路时。Dapper在其中放置一个trace context在thread-local中。trace
    context是一个小型、容易拷贝的对象,包含trace id和span id
  • 异步或者callback的时候,用统一的library封装传递
  • RPC自动继承
  • 不行,就通过api的方式接入

Dapper的流程

image-20210321215159560

写入流程分为三步。1、写入本地日志文件 2、被Dapper daemon获取 3、写入Bigtable

端到端的中位数时延在15秒。百分之75的数据的98时延在2分钟以内,但百分之25的数据的98时延可能会到几小时

为什么不在RPC接口中顺手收集信息

  • trace信息可能比rpc本身的报文要大
  • 对于异步流程,无法收集

Trace消耗的性能

在应用程序侧

创建root span耗时约204纳秒,非root span耗时176纳秒

日志文件的消耗,可并行批量

image-20210321215727742

Trace收集

每个trace数据约426byte

仅占用千分之一的Google生产带宽

自适应采样

测试环境或低量请求多采样。请求量大小采样

远端采样

远端采样,降低服务端风险。

Dapper索引选择

基于服务、机器、时间

Dapper的典型使用流程

  • 1用户选择服务、时间、期望看到的指标值(如时延)
  • 2展示出满足这个条件的所有pattern,(trace pattern)
  • 3展示这个pattern的调用链图,
  • 4右侧是全部的采样
  • 5展示出详细的时间戳分布

Dapper实际作用

  • 发现无意义的调用
  • 是否有必要访问主库,而不是从库
  • 理解黑盒系统的依赖
  • 测试。如果你不能观测,就不能优化
  • 推断依赖
  • 判断点到点网络故障,根据请求的大小,进一步推断,谁到谁引发了网络故障。
  • 在底层share系统之上,外围按用户或其他维度统计调用
  • Dapper daemon的实时信息,可用来救火

Dapper现在做不到的

  • 合并、批量处理,无法拆开识别
  • 针对批系统,能力有限
  • 根因分析,需要联合分析,比如在annotation中携带线程池队列大小,进行多个请求的联合分析等
  • 如何和Linux内核关联,如何将错误和内核联系起来

两个超时的注释

首先看一下一下ipvsadm -h对这两个参数的注释

persistent timeout

1
2
--persistent  -p [timeout]     persistent service
Specify that a virtual service is persistent. If this option is specified, multiple requests from a client are redirected to the same real server selected for the first request. Optionally, the timeout of persistent sessions may be specified given in seconds, otherwise the default of 300 seconds will be used. This option may be used in conjunction with protocols such as SSL or FTP where it is important that clients consistently connect with the same real server.

说明这个VS是否是持久的。如果配置了这个选项,来自同一个客户端的链接(这里注意:这里的同一个客户端指的是同一个IP)会转发向相同的服务器。注释中特意提到了FTP协议。我查阅了一下资料,可能像FTP协议这种,客户端通过21端口打开控制连接,再通过20端口打开数据连接,这种协议,要求来自同一个客户端ip,不同端口的请求也送向同一个服务器,估计是这个参数存在的核心原因。如果是现在的系统,比如k8s使用ipvs,这个参数是完全没必要配置的

connection timeout

1
2
--set tcp tcpfin udp
Change the timeout values used for IPVS connections. This command always takes 3 parameters, representing the timeout values (in seconds) for TCP sessions, TCP sessions after receiving a FIN packet, and UDP packets, respectively. A timeout value 0 means that the current timeout value of the corresponding entry is preserved.

更改用于ipvs连接的超时值。此命令始终使用3个参数,分别表示tcp会话,接收到FIN包的TCP会话和UDP包的超时值。单位为秒。设置为0并不代表将超时值设置为0,而是保持原有不变。顺便来说,timeout的默认值是900、120、300.

区别

一个以客户端ip为维度,一个以客户端ip+port为维度

联系:

  • persistent值大于等于set时,persistent timeout以persistent的设置为准。
  • persistent值小于set时,当set超时,但persistent超时后,会将persistent再次设置为60。只到set超时为止。所以这个时候,真实生效的persistent timeout是(s/60)*60 + p%60 + 60

用了几个小时读完了Gorilla这篇经典的 时序数据库论文
,prometheus的时序数据库在很多地方都参考了这篇论文。以此文总结一下读后感,非论文翻译。截图基本都出自于论文。本论文可以解答如下的普罗问题

为什么普罗不支持字符串类型,只支持double作为监控值

为了压缩数据,普罗使用了高效的用于double的压缩算法。

为什么普罗的默认的落盘间隔是2个小时

根据这篇论文,2个小时或以上的block的压缩比更小

普罗data盘里的文件都是用来干啥的?

有索引文件、数据文件、恢复日志等

顺带一提,Gorilla是大猩猩的意思,也是银魂中近藤勋的绰号。

Facebook因为从HBase读取时间序列太慢,再加上扩展性已经无法满足需求。Facebook对时延迟要求如此之低,Facebook否决了所有依赖磁盘做数据存储查询的方案,希望数据查询从内存返回。最终从论文看来,查询比HBase快了300多倍。

数据的编码方式

Facebook想要把数据都放到内存中,prometheus号称单机可处理数百万序列,如果按照业务代码的模式书写,三百万序列在1个小时内要占用多少内存呢?时间戳long值4byte,字符串名称加维度20byte,值算double类型,8byte,总共是32byte,两个小时,假设1分钟一个点,共有120个点。共1.2G内存。仅仅纯数据就占用了1.2G内存。Facebook基于如下两个监控数据的特点,对数据进行了高效压缩,缩小12倍原数据的大小

  • 大多数监控数据往往相差固定的时间间隔,而其他监控数据,虽然不是严格固定间隔,也基本接近固定的时间间隔。这个比例在Facebook的监控数据是24:1,即百分之96的数据都是固定间隔上报的
  • 大多数监控数据监控的值变化缓慢

基于这两点假设,它们对时间戳和值提出了两种压缩算法

时间戳

对时间戳的差值的差值进行存储,压缩空间大小。

image-20210313174810933

先存储这一块的起始时间2015 02:00:00,对于第一个数值2015 02:01:02记录差值62,对下一个值2015 02:02:02,其的差值是60,差值的差值是
60-62=-2,只存储-2即可,这样子大大节约了存储时间戳的空间

监控值

监控值采用异或的手法进行压缩,会得到0很多的二进制串,再通过合理的编码,降低总字节数。根据论文,有超过一半的数据相较上一个值没什么变化,使用一个字节即可存储

image-20210313175320885

那么这样一个block里面应该存储多久的数据呢?这是一个权衡,如果存储很久的数据,则每次查询都需要查询出很large的值,才能获得结果,如果存储数据较短,则难以达到很高的压缩比。最终他们选择了两小时。

block时长和压缩比的关系

对于普罗里的指标名称及维度,在普罗里,一个指标名称加上一组维度称为一个时间序列,Gorilla论文并没有提及维度的概念,仅仅使用名称。这块普罗实际和Gorilla都通过码表的方式,通过将字符串映射为一个longId,来大大降低存储的字节数。

对于普罗的查询流程,是 指标名称+维度=》一组时间序列,然后分别查询其中的值。Gorilla自身没有包装这一个流程,需要客户端组网自己想要查询的时间序列列表。

基于时序监控系统里的,新数据比旧数据关键,Gorilla也会将旧数据落盘。

Gorilla的高可用

单机可靠性

Facebook调查了自己之前的监控系统,发现百分之85以上请求都只查询了26个小时以内的数据。在Gorilla的第一版中,他们决定只支持26个小时数据的查询,将2小时的数据放在内存中。超过2小时的数据,会存储在高可用磁盘上,如GlusterFs、HDFS等。2小时以内的数据,有一个log用来做重启时恢复数据来保证可靠性。这样就保证了单机重启的可靠性
注:这个日志不保证能恢复所有的数据,允许异常场景下有数据丢失

Region宕机的可靠性

对于每一个Gorilla,都会主备部署一个对等的位于不同Region的实例,这两个实例都会存储数据,但数据并不完全一致。对于用户来说,他们接入距离他们最近的Gorilla实例。一旦其中一个Gorilla宕机,另一个Gorilla将会接管它的工作。为了保证数据的准确,待其恢复后26小时(拥有了它该拥有的全量数据),才可以接受业务请求。

Gorilla的扩展

Gorilla选择了水平扩展的方式,根据指标名做分区,分区到不同的主备Gorilla

论文还提到了一些其他有用的信息

监控时序数据的特点

写请求占大多数

读请求很少,人工读取,或者一些自动化告警系统

注重状态的变化

内存突然上升,乃至于一个指标值的导数突然上升等

监控系统的目标

高可用

出现问题的时候,监控系统和业务系统同时宕机是什么体验?是一把黑的体验。

低延迟

可容错

容忍单点故障灯

可扩展

随着业务系统的扩展,监控系统也需要扩展

RUM定理的背景

现在的基础设备复杂,多种多样。数据的保存和查询也多种多样,很多时候会为了很小的差异重新设计数据结构。这篇论文主要是指出了无论如何设计数据结构,都不可能在Read(读取)、Update(更新)、Memory(存储)三个方向上都做到最优,也希望指导接下来其他数据结构的设计,更希望能有一种自适应的系统,可以根据数据的查询、数据的写入、配置的硬件、人工的配置在Read、Update、Memory之间权衡。

RUM开销介绍

我们将存储在数据中心的数据称作基础数据。那些用来辅助写入,辅助查询的数据成为辅助数据

读开销 RO

也称作读放大,通过读取到的辅助数据加上基础数据除以基础数据来计算。举个例子,在mysql中查询数据,中间经过的B树层级就是读放大

更新开销 UO

也叫做写放大,实例物理上写入磁盘的大小除以逻辑上需要更新的大小。

内存开销 MO

也叫做空间放大,全部的基础数据加上全部的辅助数据除以基础数据。

RUM不可能达成举例

我们选择一个有代表性的基础数据:一个整数数组。我们将这个数据集合组织到N个块中的固定大小的元素,每一个持有一个数值。每一个块可以用一个单调递增的ID来指示。工作负载使用数据的方式有 点查询、点更新、插入和删除。

最小化RO

那我们就把bk的Id当做我们数据结构数组的下标,举例子,{1, 17}是两个元素的id,我们就开辟大小为17的数组array,然后通过array[i]来得到i的数据。现在已经达成了RO最小,但我们的索引非常稀疏,理论上我们的数组是无限大的。更新需要操作两次,将旧的数组元素置空,然后将新的数据存放在新的block中。

RO: 1 UO: 2 MO: 无穷大

最小化UO

为了最小化UO,我们将每次更新的数据直接插入到日志的最尾端,就算更新完成。查询需要遍历原来的数据和整个log文件。

UO: 1 RO: 无穷大 MO: 无穷大

最小化MO

最小化MO时,不存储辅助数据,而将基础数据密集地存储起来。 读取需要进行全表扫描,如果任意更新,也需要进行全表扫描

MO: 1 RO: N UO: 1

数据结构的RUM

image-20210314164113082

参数 N m B P T MEM
含义 数据集大小 查询结果大小 块大小 分区大小 LSM级别比例 内存
单位 元组 元组
批量创建 索引大小 点查询 范围查询(大小m) 查询/更新/删除
读取方式
B+树
完美哈希索引
ZoneMaps
层级LSM树
排序列
未排序列

image-20210314164933209

论文作者对将来系统的设想

作者认为,将来的系统应该是RUM可调的,来满足大多数场景的需要。通过一套可以轻松适应不同优化目标的访问方法来展望未来的数据系统。例如:

• 具有动态调整参数(包括树高,节点大小和拆分条件)的B +树,以便在运行时调整树大小,读取成本和更新成本。

• Approximate (tree) indexing that supports updates with low read performance overhead, by absorbing them in updatable probabilistic data structures (like quotient filters).

• Morphingaccessmethods,combiningmultipleshapesatonce. Adding structure to data gradually with incoming queries, and building supporting index structures when further data reorganization becomes infeasible.

• Update-friendly bitmap indexes, where updates are absorbed using additional, highly compressible, bitvectors which are gradually merged.

• Accessmethodswithiterativelogsenhancedbyprobabilistic data structures that allows for more efficient reads and up- dates by avoiding accessing unnecessary data at the expense of additional space.

论文中其他有意思的点

  • 现代数据系统,通常在压缩的数据上运行,并尽可能晚地解压缩

前言

iptables和ipvs都是常见的转发工具,可以进行报文的转发,比如从 IPa,Port1的消息,经过转发机器发向IPb,Port2,不管是在iptables,抑或是ipvs,进行过一次转发之后,就会留下一条转发记录,iptables是在nf_conntrack,ipvs是在ipvs自己的会话管理里,后面的来自IPa,Port1的消息,就会直接发向IPb,Port2。这个时候,假如强行有一个报文,试图把IPc,Port1的消息,转发到IPb,Port2该怎么办?在四元组冲突的情况下,该转发给谁?返程的报文是什么走向。是这篇文章试图分析的问题

本文基于Linux内核版本5.4.0

出场网元介绍

image-20210311221120239

  • S: 缩写服务的意思

  • G: 缩写Gateway的意思

Iptables和Iptables冲突

准备工作

假设S1和S2都需要经过G使用33333端口发送消息到V的33333端口,我们先配置G的iptables规则:

1
2
sysctl -w net.ipv4.ip_forward=1
iptables -t nat -I POSTROUTING -p udp -s ${S的子网} -o eth0 --sport 33333 -j SNAT --to-source ${G的IP}:33333

接下来在两台S上,把发往V的报文的默认路由指向V

1
ip route add ${V的IP}/32 via ${G的IP}

实验前先执行如下命令,收集输出

1
2
~# conntrack -S
cpu=n found=0 invalid=0 ignore=0 insert=0 insert_failed=0 drop=0 early_drop=0 error=0 search_restart=0

这个命令的输出跟您的cpu相关,有几个cpu就会输出几行。接下来在V上开启抓包

1
tcpdump -port 33333 -ann

然后在两台S上执行命令发送报文,这里有个小技巧就是使用不一样长度的内容,这样tcpdump会打印报文的长度,一眼就可以看出来送达的报文是谁的

1
echo "Hello world"| nc -4u -p 33333 V的IP 33333

你会观察到,只有先发送的报文抵达了V。随后再执行conntrack -S,收集输出的时候,会发现insert_faileddrop都增加了1。

结论

iptables和iptables冲突的场景,先发送的先生效,后生效的没有发送(这一点你可以通过在G上抓包证实)。观察点就是命令conntrack -S的输出,insert_faileddrop都有所增加。

Iptables流程图

我在做这个实验的时候,顺手打开了iptables的trace功能,记录一下报文的流程

image-20210311222756033

发送失败的也会走完这个流程,然后在插入iptables规则的时候失败。

Iptables和Ipvs冲突

准备工作

因为ipvs不会和ipvs冲突,所以我们尝试构造一下ipvs和iptables冲突的场景,让我们添加上允许V经过G发送报文到S1,让我们在G上配置ipvs所需的转发规则

1
2
ipvsadm -A -u ${G的IP}:33333 -s rr
ipvsadm -a -u ${G的IP}:33333 -r ${S1的IP}:33333 -m

实验流程

从V发送报文转发到S1,然后S2通过SNAT发送报文到V,观察情况

实施前的准备观察项

  • ipvsadm -lnc观察会话

  • ipvsadm -ln –stats 观察报文统计

  • conntrack -L|grep 33333 观察会话

  • 当V发送报文到S1后,

Ipvsadm -ln -stats统计值增加了,只有ipvs的会话表里有内容,iptables会话表里没有内容。

  • 然后S2通过SNAT发送报文到V后,

ipvsadm -ln -stats统计值没有变化,iptables会话表出现内容。(这里我试过S1先返回报文做几次交互,但是是一样的结果)

但是在ipvs有效期间,通过S1不断发送报文,还是可以发送到V节点的。这个时候,会出现S1和S2同时都能发送报文到V。

  • 让我们看看报文返程(即V发送到G的报文)会发送给谁?是S1还是S2。答案是S2。

返程的报文优先匹配了会话表,发送给了S2。如果conntrack老化,那么才会发送给S1

V上来的流程

image-20210311224826893

参考

https://blog.sourcerer.io/writing-a-simple-linux-kernel-module-d9dc3762c234

编码

C文件书写

首先,先书写一个C文件,命名为kernel_first.c

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
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Robert W. Oliver II");
MODULE_DESCRIPTION("A simple example Linux module.");
MODULE_VERSION("0.01");
static int __init

/**
* Load时候触发的函数
*/
example_init(void) {
printk(KERN_INFO
"Hello, World!\n");
return 0;
}

static void __exit

/**
* Unload时候触发的函数
*/
example_exit(void) {
printk(KERN_INFO
"Goodbye, World!\n");
}

module_init(example_init);
module_exit(example_exit);
  • 请注意使用printk而不是printf。 另外,printk与printf共享的参数不同。 例如,KERN_INFO是一个标志,用于声明应为此行设置日志记录的优先级,并且不带逗号。
    内核在printk函数中对此进行了分类,以节省堆栈内存。
  • 在文件末尾,我们调用module_init和module_exit告诉内核哪些函数是在load时候执行,那些在unload的时候执行

Makefile书写

1
2
3
4
5
obj-m += kernel_first.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

注 make前面应该是Tab键

测试

执行如下命令加载模块到内核sudo insmod kernel_first.ko执行dmesg|grep -i hello,将会看到Hello world的输出。接下来卸载内核模块
sudo rmmod kernel_first,接下来运行dmesg,你将会看到Goodbye world的输出

参考

  • 数据密集型系统

Quorum介绍

Quorum模式常用于分布式场景,保证数据的一致性。其中有两个核心参数

  • Qw 代表数据写入(包括更新、删除)需要的节点数
  • Qr 代表数据读取需要的节点数

如果你总共有N个节点,那么很容易得出只要 W+R>N,那么你的读请求和写请求一定有重叠的节点,这就保证了一致性,你总是能找到最新的那个写入请求

Quorum的不一致场景

但Quorum模式并不一定是万无一失的,他在如下场景会导致不一致

  • 如果两个写操作同时发生,则无法明确先后顺序,最终需要额外的修复手段
  • 如果写操作和读操作同时发生,写操作可能仅在一部分副本上完成。此时,读取时返回旧值还是新值存在不确定性
  • 如果某些副本上已经写入成功,而其他一些副本发生写入失败(如磁盘已满),且总的成功副本数少于w,那些已成功的副本上不会做回滚。这意味着尽管这样的写操作被视为失败,后续的读操作仍可能返回新值

还有两个更加边界的场景

Sloppy Quorum

也叫做宽松的Quorum模式,就是说当N不够的情况下,可以把集群的其他节点当作Qw节点。如果采用了sloppy
quorum,写操作的w节点和读取的r节点可能完全不同,因此无法保证读写请求一定存在重叠的节点

数据的恢复场景

进行数据的恢复在所难免,如cassandra就有读修复等,如果具有新值的节点后来发生失效,但恢复数据来自某个旧值,则总的新值副本数会低于w,这就打破了之前的判定条件

前言

在ipvs中,最小连接算法是一种负载均衡算法,常见的还有轮询算法,加权轮询算法等。让我们先做个基本的假设,每个UDP会话连接上的请求量大概一致。让LB无需观测后端服务的状态,仅仅根据会话信息,做出转发到哪个后端Service的判断,事实上,lvs也目前不能根据后端服务的cpu、内存或者是其他信息做出判断。直观上来说,最小连接数很符合大家的直观感受,保证了每个工作负载上承受的业务连接数是最少的。

ipvs-lb-udp

但是,在LVS保留会话时间稍微较长的情况下,最小连接算法在扩容、升级(升级前后IP改变)、重启(重启前后IP改变)会有一些问题。

简而言之,就是LVS向后端转发UDP消息的时候,后端服务没有很好的拒绝手段,在LC模式下,导致LVS可能转发给后端服务,超过它处理能力的消息数,等到这些会话老化之后,LVS又开始转发给后端服务,超过它处理能力的消息数,如此反复,始终造成大量消息呼损,极难自愈

详细数据推导

以扩容为例,设

  • 每秒消息量 m
  • 保活时间 t
  • 旧的节点数 a
  • 新增节点数 b
  • 使用新IP的请求占比 c (0<c<1)
  • 在一段保活时间内的IP总数 d
  • 单节点处理能力为x

那么,扩容时刻,老的节点上的会话数是d/a

扩容的时候,由于老的节点存在mt/a的会话,那么新IP上来的请求都会转发向新节点,直到把新节点的连接数冲到mt/a为止,

新节点接收请求的速率: (m * c)/b
新节点连接数和老节点持平的时间点: **(d * b)/(m * c a)*

如果新节点连接数和老节点持平的时间点远远小于保活时间,这就会有问题。其他节点上的会话都是相对离散的,所以在保活时间t内,一直不断有消息进来,但新的节点一瞬间接收到了大量请求,又会在同一时间老化。在下一个周期t,又接收到大量请求,如此反复,极难自愈。这也因为lvs的udp转发不关心后端服务器是否成功处理报文,只要转发过去就算了。就是lvs无视后端的状态转发,相比tcp,至少还有是否接收tck连接,后端主动拆链等手段。

虽然LVS能限制后端服务器的连接数,但连接数限制在这个场景是不起作用的。如果您的服务满足上述的这个模式,还是建议您修改为rr算法更为适合。

0%