开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

反应式编程入门及原理 一、前言 二、反应式编程简介 三、Re

发表于 2021-11-25

一、前言

在spring cloud gateway中大量使用了spring5才引入的反应式编程(Reactive Programming)。在此背景下,本文介绍下反应式编程相关使用及原理。

二、反应式编程简介

2.1 什么是反应式编程

反应式编程(Reactive programming,Rx)最初来源于函数式语言里面的函数式反应编程(Functional Reactive programming,FRP)。后来随着微软.Net Framework增加了Reactive Extension而在主流语言中流行起来。
反应式编程是一种编程思想、编程方式,是为了简化并发编程而出现的。与传统的处理方式相比,它能够基于数据流中的事件进行反应处理。例如:a+b=c的场景,在传统编程方式下如果a、b发生变化,那么我们需要重新计算a+b来得到c的新值。而反应式编程中,我们不需要重新计算,a、b的变化事件会触发c的值自动更新。这种方式类似于我们在消息中间件中常见的发布/订阅模式。由流发布事件,而我们的代码逻辑作为订阅方基于事件进行处理,并且是异步处理的。
反应式编程中,最基本的处理单元是事件流(事件流是不可变的,对流进行操作只会返回新的流)中的事件。流中的事件包括正常事件(对象代表的数据、数据流结束标识)和异常事件(异常对象,例如Exception)。同时,只有当订阅者第一次发布者,发布者发布的事件流才会被消费,后续的订阅者只能从订阅点开始消费,但是我们可以通过背压、流控等方式控制消费。

2.2 反应式编程的优势

反应式编程的核心是基于事件流、无阻塞、异步的,使用反应式编程不需要编写底层的并发、并行代码。并且由于其声明式编写代码的方式,使得异步代码易读且易维护。
反应式编程主要优点有:
    1、整体采用了观察者模式,异步解耦,提高服务器的吞吐量。
    2、内部提出了背压(Backpressure)概念,可以控制消费的速度
    3、书写方式与迭代器,stream类似,方便使用者理解。

2.3 与非反应式编程对比

2.3.1 与迭代器对比

Iterable Observable
迭代 next() onNext()
异常 throws Exception onError()
完成 !hasNext() onCompleted()
上面表格中的 Observable 那一列表示了观察者接受到相关事件时触发的动作。如果将迭代器看作是拉模式,那观测者模式便是推模式。被观察者(Subject)主动的推送数据给订阅者(Subscriber),触发 onNext 方法,出现异常时触发onError(),完成后触onCompleted()。

2.3.2 与stream对比

事件 stream Observable
映射 map() map()
过滤 filter() filter()
与stream对比可以看出,Reactive Programming也是通过类似的数据流方式来处理订阅的数据。不同点在于stream无法控制消息发送速度,而反应式编程中如果 Publisher 发布消息太快,超过了 Subscriber 的处理速度,反应式编程提供了背压机制来控制 Publisher的速度。

三、Reactor 入门

3.1 Reactor中主要的类

Mono 实现了 org.reactivestreams.Publisher 接口,代表0到1个元素的发布者。
Flux 同样实现了 org.reactivestreams.Publisher 接口,代表0到N个元素的发表者。
Subscriber观察者,用来观察Publisher相关动作。
Subscription解耦Subscriber和Publisher。
Mono和Flux可以相互转换。多个Mono可以合并成一个Fulx,一个Flux也可以转化成Mono。

3.2 Reactor中主要的方法

创建
just,根据参数创建数据流
never,创建一个不会发出任何数据的无限运行的数据流
empty,创建一个不包含任何数据的数据流,不会无限运行。
error,创建一个订阅后立刻返回异常的数据流
concact,从多个Mono创建Flux
generate,同步、逐一的创建复杂流。重载方法支持生成状态。在方法内部的lambda中通过调用next和complete、error来指定当前循环返回的流中的元素(并不是return)。
create,支持同步、异步、批量的生成流中的元素。
zip,将多个流合并为一个流,流中的元素一一对应
delay,Mono方法,用于指定流中的第一个元素产生的延迟时间
interval,Flux方法,用于指定流中各个元素产生时间的间隔(包括第一个元素产生时间的延迟),从0开始的Long对象组成的流
justOrEmpty,Mono方法,用于指定当初始化时的值为null时返回空的流
defaultIfEmpty,Mono方法,用于指定当流中元素为空时产生的默认值
range,生成一个范围的Integer队列

转化
map,将流中的数据按照逻辑逐个映射为一个新的数据,当流是通过zip创建时,有一个元组入参,元组内元素代表zip前的各个流中的元素。
flatMap,将流中的数据按照逻辑逐个映射一个新的流,新的流之间是异步的。
take,从流中获取N个元素,有多个扩展方法。
zipMap,将当前流和另一个流合并为一个流,两个流中的元素一一对应。
mergeWith,将当前流和另一个流合并为一个流,两个流中的元素按照生成顺序合并,无对应关系。
join,将当前流和另一个流合并为一个流,流中的元素不是一一对应的关系,而是根据产生时间进行合并。
concactWith,将当前流和另一个流按声明顺序(不是元素的生成时间)链接在一起,保证第一个流消费完后再消费第二流
zipWith,将当前流和另一个流合并为一个新的流,这个流可以通过lambda表达式设定合并逻辑,并且流中元素一一对应
first,对于Mono返回多个流中,第一个产生元素的Mono。对于Flux,返回多个Flux流中第一个产生元素的Flux。
block,Mono和Flux中类似的方法,用于阻塞当前线程直到流中生成元素
toIterable,Flux方法,将Flux生成的元素返回一个迭代器
defer,Flux方法,用于从一个Lambda表达式获取结果来生成Flux,这个Lambda一般是线程阻塞的
buffer相关方法,用于将流中的元素按照时间、逻辑规则分组为多个元素集合,并且这些元素集合组成一个元素类型为集合的新流。
window,与buffer类似,但是window返回的流中元素类型还是流,而不是buffer的集合。
filter,顾名思义,返回负责规则的元素组成的新流
reduce,用于将流中的各个元素与初始值(可以设置)逐一累积,最终得到一个Mono。

其他
doOnXXX,当流发生XXX时间时的回调方法,可以有多个,类似于监听。XXX包括Subscribe、Next、Complete、Error等。
onErrorResume,设置流发生异常时返回的发布者,此方法的lambda是异常对象
onErrorReturn,设置流发生异常时返回的元素,无法捕获异常
then,返回Mono,跳过整个流的消费
ignoreElements,忽略整个流中的元素
subscribeOn,配合Scheduler使用,订阅时的线程模型。
publisherOn,配合Scheduler使用,发布时的线程模型。
retry,订阅者重试次数

3.3 一个栗子

假设有个名单列表,要根据名单获取对应名字的邮箱,并且过滤掉邮箱长度小于10的邮箱,最后再将符合条件的邮箱打印出来。

使用stream编程如下所示。

1
2
3
4
less复制代码Stream.of("Tom", "Bob", "zhangsan", "lisi")
.map(s -> s.concat("@qq.com"))
.filter(s -> s.length() > 10)
.forEach(System.out::println);

使用Reactive编程如下所示。

1
2
3
4
less复制代码Flux.just("Tom", "Bob", "zhangsan", "lisi")
.map(s -> s.concat("@qq.com"))
.filter(s -> s.length() > 10)
.subscribe(System.out::println);
通过上述例子可以看出,stream和Reactive在形式上有相似之处,都是先创建数据源,然后经过中间过程处理转换,最后再消费中间处理结果。
1
arduino复制代码Flux.just("Tom", "Bob", "zhangsan", "lisi")
Flux.just()创建一个Flux的发布者。除了使用just方法外,还有fromCallable,fromIterable等其他方式用来从不同场景中创建publisher。
1
perl复制代码map(s -> s.concat("@qq.com"))
map的含义就是映射,在上一步中创建了一个4个元素序列的发布者,在该步骤中将每个序列元素进行转换,在每个名称后面加上邮箱后缀。
1
scss复制代码filter(s -> s.length() > 10)
过滤步骤,将经过映射的4个元素进行过滤,剔除掉长度不大于10的。中间过程书写形式和含义与stream类似。
1
scss复制代码subscribe(System.out::println);
该步骤是最终的订阅阶段,之前创建的都是被观察者,该步骤是创建一个观察者subscriber。其中subscriber的具体行为就是System.out::println打印出之前处理过的元素。至此一个订阅发布的过程就结束了。

四、Reactor的工作原理

在之前的章节中已经说过,反应式编程的核心就是一个观察者模式。

观察者模式
Flux和Mono相当于观察者模式中的subject,当Flux或Mono调用subscribe方法时,相当于subject发出了一个Event,从而让订阅此事件的观察者进行消费。那Flux框架具体如何实现这套机制呢,还是以上节中的例子跟踪下它是如何工作的。

1
2
3
4
less复制代码Flux.just("Tom", "Bob", "zhangsan", "lisi")
.map(s -> s.concat("@qq.com"))
.filter(s -> s.length() > 10)
.subscribe(System.out::println);

本文基于3.1.9.RELEASE版本。

4.1 申明阶段

4.1.1 Flux.just()

进入just方法,经过若干跳转后,进入如下方法。
1
2
3
4
5
6
7
8
9
php复制代码public static <T> Flux<T> fromArray(T[] array) {
if (array.length == 0) {
return empty();
}
if (array.length == 1) {
return just(array[0]);
}
return onAssembly(new FluxArray<>(array));
}
onAssembly是一个钩子方法,暂时忽略。最终就是new FluxArray<>(array)一个对象创建出了一个FluxArray。点击FluxArray的构造函数中,可以看看到,只是把array赋值给了对象内部的array。
1
2
3
4
5
php复制代码final T[] array;
@SafeVarargs
public FluxArray(T... array) {
this.array = Objects.requireNonNull(array, "array");
}

4.1.2 map

Flux.just方法只是创建了一个FluxArray对象,回到最开始定义的地方,下一步执行的是map方法。定义如下所示。
1
2
3
4
5
6
typescript复制代码public final <V> Flux<V> map(Function<? super T, ? extends V> mapper) {
if (this instanceof Fuseable) {
return onAssembly(new FluxMapFuseable<>(this, mapper));
}
return onAssembly(new FluxMap<>(this, mapper));
}
上一步创建的FluxArray是一个Fuseable,所以执行if条件里的逻辑,创建一个FluxMapFuseable对象,FluxMapFuseable的构造函数中有两个参数,this和mapper。this就是上一步创建出来的FluxArray,mapper就是我们自定义的Lambda表达式,即:s -> s.concat("@qq.com")。再点击进入FluxMapFuseable的构造函数中。
1
2
3
4
5
javascript复制代码FluxMapFuseable(Flux<? extends T> source,
Function<? super T, ? extends R> mapper) {
super(source);
this.mapper = Objects.requireNonNull(mapper, "mapper");
}
从这个构造函数可以看出,source是上一步骤just得到的FluxArray,mapper是对应map的Lambda表达式,所以当执行map操作的时候,其实是又将FluxArray进行封装,得到了一个新的FluxMapFuseable对象。

4.1.3 filter

再次回到开始的申明地方,在执行完map操作后,接着执行filter。同理,点击filter方法,可以看到如下代码。
1
2
3
4
5
6
kotlin复制代码public final Flux<T> filter(Predicate<? super T> p) {
if (this instanceof Fuseable) {
return onAssembly(new FluxFilterFuseable<>(this, p));
}
return onAssembly(new FluxFilter<>(this, p));
}
在看过map的操作后,这一步骤其实就相当熟悉了,filter步骤将上一步map操作得到的FluxMapFuseable方法又一次封装成了FluxFilterFuseable对象。

4.1.4 申明总结

从上面的定义可以看出,申明阶段就是一层一层的创建各种Flux对象,并没有实际执行任何操作。通过just,map,filter等操作,将发布者一层一层的封装,从最开始的FluxArray对象,到FluxMapFuseable对象以及最后的FluxFilterFuseable对象。如下图所示。

null

4.2 订阅阶段

4.2.1 subscribe、onsubscribe

上述例子中,just,map,filter只是创建了一个个的对象。并没有实际执行相关逻辑。当调用被观察者的subscribe方法时,会为被观察者添加相应的观察者,同时触发观察者相关方法,从而使整个观察者模式得以进行下去。接着看下Fulx的subscribe方法。
经过一系类的jump后,最终会调用Flux的subscribe,如下所示。
1
java复制代码public abstract void subscribe(CoreSubscriber<? super T> actual);
该方法是一个抽象方法,需要看下子类是如何实现的。还记得上一步骤中filter后产生的对象嘛?FluxFilterFuseable是Flux的一个具体实现,当调用subscribe后,会跳转到FluxFilterFuseable的subscribe方法,代码如下。
1
2
3
4
5
6
7
8
typescript复制代码public void subscribe(CoreSubscriber<? super T> actual) {
if (actual instanceof ConditionalSubscriber) {
source.subscribe(new FilterFuseableConditionalSubscriber<>((ConditionalSubscriber<? super T>) actual,
predicate)); // 1
return;
}
source.subscribe(new FilterFuseableSubscriber<>(actual, predicate)); // 2
}
传进来的actual是System.out::println,也就是我们最终执行的表达式,它被封装成了一个LambdaSubscriber观察者,predicate为filter指定的表达式s -> s.length() > 10,source为上一步骤中生成的FluxMapFuseable对象。根据对象情况,代码会走到2处。2处的逻辑就是将actual和predicate封装成一个**订阅者**去订阅source也就是FluxMapFuseable对象。
接着代码会去调用source的subscribe方法,也就是FluxMapFuseable对应的subscribe方法。
1
2
3
4
5
6
7
8
typescript复制代码public void subscribe(CoreSubscriber<? super R> actual) {
if (actual instanceof ConditionalSubscriber) { //1
ConditionalSubscriber<? super R> cs = (ConditionalSubscriber<? super R>) actual;
source.subscribe(new MapFuseableConditionalSubscriber<>(cs, mapper));
return;
}
source.subscribe(new MapFuseableSubscriber<>(actual, mapper)); //2
}
代码还是会走到2出,这里传入的actual是上一步骤中封装了System.out::println和s -> s.length() > 10的观察者,mapper为s -> s.concat("@qq.com"),从这段代码可以看出,所做的逻辑就是将上一步中的观察者和mapper又封装成了新的观察者。一层一层的套娃。
最后,看下本步骤中的source,也就是FluxArray对象的subscribe方法。
1
2
3
4
5
6
7
8
9
10
11
12
php复制代码public static <T> void subscribe(CoreSubscriber<? super T> s, T[] array) {
if (array.length == 0) {
Operators.complete(s);
return;
}
if (s instanceof ConditionalSubscriber) { // 1
s.onSubscribe(new ArrayConditionalSubscription<>((ConditionalSubscriber<? super T>) s, array));
}
else {
s.onSubscribe(new ArraySubscription<>(s, array)); // 2
}
}
FluxArray是数据的源头,传入的array为我们定义的"tom", "Bob", "zhangsan", "lisi"名字。s为上一步骤中创建的subscriber。在数据的源头可以看出作为观察者模式的触发项,该步骤中触发了观察者的onsubscribe方法。同时为了解耦观察者和被观察者,创建一个ArraySubscription对象。FluxArray的subscribe会执行2处代码,s.onSubscribe(new ArraySubscription<>(s, array)),这里的s是上一步骤中创建的MapFuseableSubscriber中的onSubscribe方法,对应代码如下所示。
1
2
3
4
5
6
7
kotlin复制代码@Override
public void onSubscribe(Subscription s) {
if (Operators.validate(this.s, s)) {
this.s = (QueueSubscription<T>) s;
actual.onSubscribe(this);
}
}
actual是FilterFuseableSubscriber对象,本质就是赋值后,然后调用FilterFuseableSubscriber的onSubscribe方法。FilterFuseableSubscriber对应的onSubscribe方法如下所示。
1
2
3
4
5
6
7
kotlin复制代码@Override
public void onSubscribe(Subscription s) {
if (Operators.validate(this.s, s)) {
this.s = (QueueSubscription<T>) s;
actual.onSubscribe(this);
}
}
和MapFuseableSubscriber类似。actual对应的是LambdaSubscriber,也就是System.out::println。LambdaSubscriber的onsubscribe如下所示。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
scss复制代码public final void onSubscribe(Subscription s) {
if (Operators.validate(subscription, s)) {
this.subscription = s;
if (subscriptionConsumer != null) {
try {
subscriptionConsumer.accept(s); // 1
}
catch (Throwable t) {
Exceptions.throwIfFatal(t);
s.cancel();
onError(t);
}
}
else {
s.request(Long.MAX_VALUE); // 2
}
}
}
1和2代码的最终逻辑都一样,都会执行request方法。背压的原理就是通过这个request来实现的,观察者可以通过request来指定一次性订阅多少数据。
总结一下:一个subscribe方法其实是创建了三个观察者,与创建发布者类似,创建的观察者也是一层一层嵌套。从最外层的subscriber与上一层的操作结合生成一个新的subscriber。再继续向上调用,最终调用到数据源头。然后从数据源头开始一层一层再出发观察者的onsubscribe。

null

4.2.2 request

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
scss复制代码public final void onSubscribe(Subscription s) {
if (Operators.validate(subscription, s)) {
this.subscription = s;
if (subscriptionConsumer != null) {
try {
subscriptionConsumer.accept(s); // 1
}
catch (Throwable t) {
Exceptions.throwIfFatal(t);
s.cancel();
onError(t);
}
}
else {
s.request(Long.MAX_VALUE); // 2
}
}
}
在onsubscribe阶段最终会调用s的request方法。还记得s嘛?s是在解耦观察者和被观察这创建出来的subscription。
1
2
3
4
5
6
7
8
9
10
11
12
php复制代码public static <T> void subscribe(CoreSubscriber<? super T> s, T[] array) {
if (array.length == 0) {
Operators.complete(s);
return;
}
if (s instanceof ConditionalSubscriber) { // 1
s.onSubscribe(new ArrayConditionalSubscription<>((ConditionalSubscriber<? super T>) s, array));
}
else {
s.onSubscribe(new ArraySubscription<>(s, array)); // 2
}
}
就是这里的ArraySubscription对象。看下这个对象的request方法。
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
ini复制代码@Override
public void request(long n) {
if (Operators.validate(n)) {
if (Operators.addCap(REQUESTED, this, n) == 0) {
if (n == Long.MAX_VALUE) {
fastPath(); // 1
}
else {
slowPath(n); // 2
}
}
}
}

void fastPath() {
final T[] a = array;
final int len = a.length;
final Subscriber<? super T> s = actual;

for (int i = index; i != len; i++) {
if (cancelled) {
return;
}

T t = a[i];

if (t == null) {
s.onError(new NullPointerException("The " + i + "th array element was null"));
return;
}

s.onNext(t);
}
if (cancelled) {
return;
}
s.onComplete();
}
直接看下fastPath(),代码都贴在了一起。到这里就真正开始消费。通过一个for循环,调用Subscriber的onNext方法,onNext方法执行完毕后,执行Subscriber的onComplete方法。这里的s是MapFuseableConditionalSubscriber,看下它的onNext方法。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
kotlin复制代码public void onNext(T t) {
if (sourceMode == ASYNC) {
actual.onNext(null);
}
else {
if (done) {
Operators.onNextDropped(t, actual.currentContext());
return;
}
R v;

try {
v = Objects.requireNonNull(mapper.apply(t),
"The mapper returned a null value."); // 1
}
catch (Throwable e) {
onError(Operators.onOperatorError(s, e, t, actual.currentContext()));
return;
}

actual.onNext(v); // 2
}
}
在1处执行mapper对应的Lambda表达式,在2处执行下一步的Subscriber的onNext方法。下一步是Filter,再下一步是最终的System.out::println。最后onNext都执行换成后,执行s的onComplete方法,道理也是一样的,都是从最开始Subscriber的onComplete方法一层一层执行。至此一个完成的观察者模式的执行情况就完成了。

4.3 总结

1、申明阶段只是创建了一个个的被观察者,把动作包装成对象,其他什么事都没做,直到调用被观察者的subscribe方法,为被观察者添加观察者。
2、添加观察者后,每一个申明步骤都会创建一个新的观察者,观察上一步骤的被观察者,直到最外层被观察者触发onSubscribe,接着按照刚才添加的观察者一层层调用对应的onSubscribe方法,最后触发request方法。
3、当触发到最外层的request后,就执行真正的逻辑,再一层层调用观察者的onNext方法。最后完成后调用onComplete方法。

Q1:反应式编程的背压怎么实现的?
A1:观察者通过request中的传参,控制消费速度,从而实现反应式编程的背压特性,在声明Subscriber时,我们可以重写Subscriber接口,实现里面的request方法,来时控制订阅的速度。

Q2:为什么反应式编程可以提高吞吐量?
A2:从刚才的实现逻辑可以看出,被观察者只是申明了数据操作的定义,实际上什么都没做,几乎不消耗cpu,io等资源。在使用反应式编程处理web请求时,一般会写成如下形式。

1
2
3
4
5
6
typescript复制代码@RequestMapping("/mail")
public Flux<String> getUserMail() {
return Flux.just("Tom", "Bob", "zhangsan", "lisi")
.map(s -> s.concat("@qq.com"))
.filter(s -> s.length() > 10)
}

只是定义了被观察者,能够很快完成一个请求的处理。
因此在实际应用中,如http请求,短时间如果有大量请求到来,可以快速创建相关请求,增大接口的吞吐量,但是接口的处理速度并不会加快,因为需要处理的请求耗时,都不会减小。

Q3:代码中如何控制订阅的速度?
A3:如果直接使用subecribe(System.out::println),默认走的request(Long.MAX_VALUE)。如果要控制速度有如下几种方式。
1、自己实现Subscriber,上述例子中使用了subecribe(System.out::println),默认创建了一个LambdaSubscriber观察者,在该观察者的onSubscribe方法中执行了s.request(Long.MAX_VALUE);方法,所以可以自己实现一个Subscriber自定义策略给request中传入不同的值,从而控制速度。比如发现当前流量大于500时,限制速度。

1
2
3
4
5
6
7
arduino复制代码public final void onSubscribe(Subscription s) {
if (rate > 500) {
s.request(500)
} else {
s.request(Long.MAX_VALUE);
}
}

2、通过flux的limitrate方式实现调整request数量

1
2
3
4
scss复制代码Flux.range(1,10)
.log()
.limitRate(2)
.subscribe();

3、实现现有BaseSubscriber类,重写里面的onSubscribe方法,本质跟1类似。

五、参考文档

cloud.tencent.com/developer/a…
blog.yannxia.top/2018/06/26/…
www.jianshu.com/p/7ee89f70d…
www.jianshu.com/p/df395eb28…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Java代理模式之Java中介者模式 Java中介者模式

发表于 2021-11-25

「这是我参与11月更文挑战的第25天,活动详情查看:2021最后一次更文挑战」

Java中介者模式

中介者模式(Mediator Pattern)是用来降低多个对象和类之间的通信复杂性。这种模式提供了一个中介类,该类通常处理不同类之间的通信,并支持松耦合,使代码易于维护。中介者模式属于行为型模式。

介绍

意图:用一个中介对象来封装一系列的对象交互,中介者使各对象不需要显式地相互引用,从而使其耦合松散,而且可以独立地改变它们之间的交互。

主要解决:对象与对象之间存在大量的关联关系,这样势必会导致系统的结构变得很复杂,同时若一个对象发生改变,我们也需要跟踪与之相关联的对象,同时做出相应的处理。

何时使用:多个类相互耦合,形成了网状结构。

如何解决:将上述网状结构分离为星型结构。

关键代码:对象 Colleague 之间的通信封装到一个类中单独处理。

应用实例: ① 中国加入 WTO 之前是各个国家相互贸易,结构复杂,现在是各个国家通过 WTO 来互相贸易。 ② 机场调度系统。 ③ MVC 框架,其中C(控制器)就是 M(模型)和 V(视图)的中介者。

优点: ① 降低了类的复杂度,将一对多转化成了一对一。 ② 各个类之间的解耦。 ③ 符合迪米特原则。

缺点:中介者会庞大,变得复杂难以维护。

使用场景: ① 系统中对象之间存在比较复杂的引用关系,导致它们之间的依赖关系结构混乱而且难以复用该对象。 ② 想通过一个中间类来封装多个类中的行为,而又不想生成太多的子类。

注意事项:不应当在职责混乱的时候使用。

实现

我们通过聊天室实例来演示中介者模式。实例中,多个用户可以向聊天室发送消息,聊天室向所有的用户显示消息。我们将创建两个类 ChatRoom 和 User。User 对象使用 ChatRoom 方法来分享他们的消息。

MediatorPatternDemo,我们的演示类使用 User 对象来显示他们之间的通信。

步骤 1

创建中介类。

1
2
3
4
5
6
7
8
typescript复制代码import java.util.Date;

public class ChatRoom {
public static void showMessage(User user, String message){
System.out.println(new Date().toString()
+ " [" + user.getName() +"] : " + message);
}
}

步骤 2

创建 user 类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typescript复制代码public class User {
private String name;

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public User(String name){
this.name = name;
}

public void sendMessage(String message){
ChatRoom.showMessage(this,message);
}
}

步骤 3

使用 User 对象来显示他们之间的通信。

1
2
3
4
5
6
7
8
9
java复制代码public class MediatorPatternDemo {
public static void main(String[] args) {
User robert = new User("Robert");
User john = new User("John");

robert.sendMessage("Hi! John!");
john.sendMessage("Hello! Robert!");
}
}

步骤 4

执行程序,输出结果:

1
2
ini复制代码Thu Jan 31 16:05:46 IST 2013 [Robert] : Hi! John!
Thu Jan 31 16:05:46 IST 2013 [John] : Hello! Robert!

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

三维重建概述和基于Pixel2Mesh的3D重建技术

发表于 2021-11-25

一、前言

客观世界中的物体都是三维的,真实地描述和显示客观世界中的三维物体是计算机图形学研究的重要内容。三维重建又是计算机图形图像的核心技术之一,应用领域非常广泛。例如,医疗行业可以使用生物器官的3D 模型仿真手术解剖或辅助治疗;电影娱乐业可以使用3D模型实现人物和动物的动画以及动态模拟;建筑行业可以使用3D建筑模型来验证建筑物和景观设计的空间合理性、美学视觉效果等等。目前,大家大多在研究模型识别,但这只是计算机视觉的一部分,真正意义上的计算机视觉要超越二维,感知到三维环境[1]。我们生活在三维空间里,要做到更高效的交互和感知,也须将世界恢复到三维。

二、常见三维表示方法介绍

机屏幕本身是二维平面的,我们之所以能欣赏到真如实物般的三维图像[2],是因为显示在计算机屏幕上色彩和灰度的不同而产生错觉,将二维屏幕感知为三维环境。由色彩学可知,三维物体边缘的凸出部分一般显高亮度色,而凹下去的部分由于受光线的遮挡而略显暗色,加之人眼也有近大远小的成像特性,就会形成3D立体感。对于如何在计算机中表示出3D模型效果,根据不同的使用需求有不同的表示方法,基本可以分为四类,深度图(Depth Map)、点云(Point Cloud)[3]、体素(Voxel)[4]和网格(Mesh)[5]。

2.1 深度图(Depth Map)

image.png

Fig.1 立方体深度示意图

深度图是一张2D图片,每个像素都记录了从视点(Viewpoint)到遮挡物表面(遮挡物就是阴影生成物体)的距离,相当于只有三维信息里Z轴上的信息,这些像素对应的顶点对于观察者而言是“可见的”。

2.2 体素(Voxel)

image.png

Fig.2 游戏《我的世界》

体素或立体像素是体积像素(VolumePixel)的简称,是模型数据于三维空间上的最小单位,是一种规则数据。体素概念上类似于二维图像中的像素,其本身并不含有空间中位置的数据(即它们的座标),然而却可以从它们相对于其他体素的位置来推敲。如图Fig.2,是比较流行的一款PC端3D游戏《我的世界》,玩家可以在自己的世界中将一个个体素块任意堆叠,构建出自己专属的、个性的3D人物和世界。

2.3 网格(Mesh)

image.png

Fig.3 海豚网格图

多边形网格(Polygonmesh)是三维计算机图形学中表示多面体形状的顶点与多边形的集合,是一种非规则结构数据。这些网格通常由三角形、四边形或者其它的简单凸多边形组成。其中,最常用的是三角网格,三角网格通常需要存储三类信息:顶点、边和面。

顶点: 每个三角网格都有三个顶点,各顶点都有可能和其他三角网格共享。

边: 连接两个顶点的边,每个三角网格有三条边。

面: 每个三角网格对应一个面,我们可以用顶点或边的列表来表示面。

2.4 点云(Point Cloud)

image.png

Fig.4 甜甜圈式点云图

点云是指以点的形式记录的数据。每一个点包含了丰富的信息,包括三维坐标X、Y、Z、颜色、分类值、强度值、时间等等。点云可以将现实世界原子化,通过高精度的点云数据可以还原现实世界。

那选哪个作为我们常用的3D模型表示呢?根据介绍我们可以知道Voxel,受到分辨率和表达能力的限制,会缺乏很多细节;Point Cloud,点之间没有连接关系,会缺乏物体的表面信息;相比较而言Mesh的表示方法具有轻量、形状细节丰富的特点。

三维重建技术上大体可分为接触式和非接触式两种。其中,较常见的是非接触式中基于主动视觉的如激光扫描法、结构光法、阴影法和Kinect技术等,和基于机器学习的如统计学习法、神经网络法、深度学习与语义法等。

三、基于主动视觉的三维重建技术[7]

3.1 激光扫描法

激光扫描法其实就是利用激光测距仪来进行真实场景的测量。首先,激光测距仪发射光束到物体的表面,然后,根据接收信号和发送信号的时间差确定物体离激光测距仪的距离,从而获得测量物体的大小和形状。

3.2 结构光法

结构光法的原理是首先按照标定准则将投影设备、图像采集设备和待测物体组成一个三维重建系统;其次,在测量物体表面和参考平面分别投影具有某种规律的结构光图;然后再使用视觉传感器进行图像采集,从而获得待测物体表面以及物体的参考平面的结构光图像投影信息;最后,利用三角测量原理、图像处理等技术对获取到的图像数据进行处理,计算出物体表面的深度信息,从而实现二维图像到三维图像的转换。按照投影图像的不同,结构光法可分为:点结构光法、线结构光法、面结构光法、网络结构光和彩色结构光。

3.3 阴影法

阴影法是一种简单、可靠、低功耗的重建物体三维模型的方法。这是一种基于弱结构光的方法,与传统的结构光法相比,这种方法要求非常低,只需要将一台相机面向被灯光照射的物体,通过移动光源前面的物体来捕获移动的阴影,再观察阴影的空间位置,从而重建出物体的三维结构模型。

3.4 Kinect技术

Kinect传感器是最近几年发展比较迅速的一种消费级的3D摄像机,它是直接利用镭射光散斑测距的方法获取场景的深度信息,Kinect传感器如下图所示。Kinect传感器中间的镜头为摄像机,左右两端的镜头被称为3D深度感应器,具有追焦的功能,可以同时获取深度信息、彩色信息、以及其他信息等。Kinect在使用前需要进行提前标定,大多数标定都采用张正友标定法。

四、基于Pixel2Mesh的三维重建技术

Pixel2Mesh(Generating3D Mesh Models From Single RGB Images)

4.1 整体框架

image.png

Fig.5 网络架构图

1.首先给定一张输入图像:InputImage。

2.为任意的输入图像都初始化一个固定大小的椭球体(三轴半径分别为0.2、0.2、0.8m)作为其初始三维形状:Ellipsoid Mesh。

3.整个网络可以分成上下两个部分:图像特征提取网络和级联网格变形网络。

1上面部分负责用全卷积神经网络(CNN)[8]提取输入图像的特征。

2下面部分负责用图卷积神经网络(GCN)[9]来提取三维mesh特征,并不断地对三维mesh进行形变,逐步将椭球网格变形为所需的三维模型,目标是得到最终的飞机模型。

4.注意到图中的PerceptualFeature Pooling层将上面的2D图像信息和下面的3Dmesh信息联系在了一起,即通过借鉴2D图像特征来调整3D mesh中的图卷积网络的节点状态。这个过程可以看成是Mesh Deformation。

5.还有一个很关键的组成是GraphUnpooling。这个模块是为了让图节点依次增加,从图中可以直接看到节点数是由156–>628–>2466变换的,这其实就Coarse-To-Fine的体现。

4.2 图卷积神经网络GCN

我们先来看看图卷积神经网络[6]是如何提取特征的。一般的,对欧几里得空间中一维序列的语音数据和二维矩阵的图像数据我们会分别采取RNN和CNN两种神经网络来提取特征,那GCN其实就是针数据结构中另一种形式图的结构做特征提取。GCN在表示3D结构上具有天然的优势,由前面我们知道3D mesh是由顶点、边和面来描述三维对象的,这正好对应于图卷积神经网络G =(V,E,F),分别为顶点Vertex、边Edge和特征向量Feature。

图卷积的公式定义如下:

image.png

其中,flp、fl+1p分别表示顶点p在卷积操作前后的特征向量;N(p)指的是顶点的p邻居节点;w1、w2表示待学习的参数。

图卷积的隐藏层表示如下:

image.png

其中,f表示一种传播规则;每个隐藏层Hi都对应一个维度为Nxfi的形状特征矩阵(N是图数据中的节点个数,fi表示每个节点的输入特征数),该矩阵中的每一行代表的是该行对应节点的fi维特征表征;A是NxN的邻接矩阵。加上第i层的权重矩阵fixfi+1,则有输入层为Nxfi时,输出维度为NxNxNxfixfi+1,即为Nxfi+1。在每一个隐藏层中,GCN会使用传播规则f将这些信息聚合起来,从而形成下一层的特征,这样在每个连续层中图结构的特征就会变得越来越抽象。

从以上两式可以看出图卷积神经网络的节点是根据自身特征和邻接节点的特征来进行更新的。

4.3 Mesh Deformation Block

作用:输入2D CNN特征和3D顶点位置、形状特征,输出新3D顶点位置、形状特征

image.png

Fig.6 MeshDeformation Block

为了生成与输入图像中显示物体所对应的3D mesh模型,Mesh Deformation Block需要从输入图像中引入2D CNN特征(即图示P),这需要将图像特征网络和当前网格模型中的顶点位置(Ci-1)相融合。然后将上述融合的特征与附着在输入图顶点上的mesh形状特征(Fi-1)级联起来,合并输入到基于G-ResNet模块中。G-ResNet是基于图结构的ResNet网络,为每个顶点生成新的顶点位置坐标(Ci)和3D形状特征(Fi)。

4.4 Perceptual Feature Pooling Layer

作用:将3D顶点位置和2D CNN特征融合

image.png

Fig.7 PerceptualFeature Pooling Layer

该模块根据三维顶点坐标从图像特征P中提取对应的信息,然后将提取到的各个顶点特征再与上一时刻的顶点特征做融合。具体做法是假设给定一个顶点的三维坐标,利用摄像机内参计算该顶点在输入图像平面上的二维投影,然后利用双线性插值将相邻四个像素点的特征集中起来,就可以输入到GCN中提取图结构特征。特别的是,将从“conv3_3”、“conv4_3”和“conv5_3”层提取的特征级联起来,得到总通道数为1280(256+512+512)。然后将该感知特征与来自输入网格的128维3D特征相连接,从而得到1408的总维数。

4.5 G-ResNet

作用:用来提取图结构中的特征

在获得能表征三维mesh信息和二维图像信息的各顶点1408维特征后,该模型设计了一个基于ResNet结构的GCN来预测每个顶点新的位置和形状特征,这需要更高效的交换顶点之间的信息[10]。然而,如四(2)中GCN介绍,每个卷积只允许相邻像素之间的特征交换,这大大降低了信息交换的效率。为了解决这个问题,通过短连接的的方法构建了一个非常深G-ResNet网络。在这个框架中,所有block的G-ReNet具有相同的结构,由14个128通道的图残差网络层组成。

4.6 Graph Unpooling Layer

作用:增加GCNN的顶点数目

image.png

Fig.8 GraphUnpooling示意图

因为每一个图卷积block本身顶点数量是固定的,它允许我们从一个顶点较少的网格开始,只在必要时添加更多的顶点,这样可以减少内存开销并产生更好的结果。一个简单的方法是在每个三角形的中心添加一个顶点[11],并将其与三角形的三个顶点连接。但是,这会导致顶点度数不平衡。受计算机图形学中普遍使用的网格细分算法中顶点添加策略的启发,我们在每条边的中心添加一个顶点,并将其与该边的两个端点连接起来(如Fig.8.a)。新添加顶点的三维特征设置为其两个相邻顶点的平均值,如果将三个顶点添加到同一个三角形(虚线)上,我们还将它们连接起来。因此,我们为原始网格中的每个三角形创建4个新三角形,并且顶点的数量将随着原始网格中边的数量而均匀增加。

4.7 Loss

1.Chamfer Loss 倒角损失[3]

image.png

Chamfer distance倒角距离是指两点之间的距离lc,p表示某具体节点,q为p节点的邻居节点,目的是约束网格顶点之间的位置,将顶点回归到其正确方位,但是并不足以产生良好的3D网格。

2.Normal Loss 法向损失

image.png

Normal loss需要顶点与其相邻顶点之间的边垂直于可观测到的网格真值,优化这一损失相当于强迫局部拟合切平面的法线与观测值一致。

3.LaplacianRegularization 拉普拉斯正则化

image.png

Laplacian Regularization鼓励相邻的顶点具有相同的移动,防止顶点过于自由移动而避免网格自交,保持变形过程中相邻顶点之间的相对位置。

4.Edge Length Regularization 边长正则化

image.png

Edge Length Regularization作用是防止产生离群点,顶点间距离偏差过大从而约束边长。

最终loss为:

image.png

image.png

五、小结

image.png

Fig.9 Results

受深度神经网络的限制,此前的方法多是通过Voxel和Point Cloud呈现,而将其转化为Mesh并不是一件易事,Pixel2Mesh则利用基于图结构的神经网络逐渐变形椭圆体来产生正确的几何形状。本文着重介绍了3D mesh重建的背景、表示方法和Pixel2Mesh算法。文章贡献归纳如下:

(1)实现了用端到端的神经网络实现了从单张彩色图直接生成用mesh表示的物体三维数据

(2)采用图卷积神经网络来表示3D mesh信息,利用从输入图像提取到的特征逐渐对椭圆进行变形从而产生正确的几何形状

(3)为了让整个形变的过程更加稳定,文章还采用Coarse-To-Fine从粗粒度到细粒度的方式

(4)为生成的mesh设计了几种不同的损失函数来让整个模型生成的效果更加好

image.png

Fig.10 飞机Mesh效果

image.png

Fig.11 凳子Mes h 效果

Future work 该算法所应用的领域是物体的3D模型重建,可以期待将其扩展为更一般的情况,如场景级重建,并学习多图像的多视图重建(Piexl2Mesh++)[12]。

参考文献:

  1. Zhiqin Chen and HaoZhang. Learning implicit fifields for generative shape modeling. In Proceedings of the IEEE Conference on Computer Visionand Pattern Recognition, pages 5939–5948, 2019.
  2. Angjoo Kanazawa,Shubham Tulsiani, Alexei A. Efros, and Jitendra Malik. Learningcategory-specifific mesh reconstruction from image collections. In ECCV, 2018.
  3. Fan, H., Su, H., Guibas,L.J.: A point set generation network for 3d object reconstruction from a singleimage. In CVPR, 2017.
  4. Choy, C.B., Xu, D., Gwak,J., Chen, K., Savarese, S.: 3d-r2n2: A unifified approach for single andmulti-view 3d object reconstruction. In ECCV, 2016.
  5. Rohit Girdhar, DavidF. Fouhey, Mikel Rodriguez, and Abhinav Gupta. Learning a predictable andgenerative vector representation for objects. In ECCV, 2016.
  6. Thomas N. Kipf and MaxWelling. Semi-supervised classifification with graph convolutional networks.In ICLR, 2016.
  7. 郑太雄, 黄帅, 李永福, 冯明驰. 基于视觉的三维重建关键技术研究综述. 自动化学报, 2020, 46(4): 631-652. doi:10.16383/j.aas.2017.c170502
  8. Lars Mescheder, MichaelOechsle, Michael Niemeyer, Sebastian Nowozin, and Andreas Geiger. Occupancynetworks: Learning 3d reconstruction in function space. In CVPR, pages 4460–4470, 2019.
  9. Peng-Shuai Wang, Yang Liu,Yu-Xiao Guo, Chun-Yu Sun, and Xin Tong. O-cnn: Octree-based convolutional neuralnetworks for 3d shape analysis. ACMTransactions on Graphics (TOG) , 36(4):72, 2017.
  10. Christian Hane,Shubham Tulsiani, and Jitendra Malik. Hierarchical surface prediction for 3dobject reconstruction. In 3DV,2017.
  11. Sunghoon Im, Hae-GonJeon, Stephen Lin, and In SoKweon. Dpsnet: End-to-end deep plane sweep stereo.In ICLR, 2018.
  12. Chao Wen and Yinda Zhangand Zhuwen Li and Yanwei Fu: Multi-View 3D Mesh Generation via Deformation. InECCV, 2019.

文章来自一点资讯AI图像图形实验室(AIIG)团队

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

Redis持久化机制 RDB AOF

发表于 2021-11-25

RDB

1. 什么是RDB

RDB:每隔一段时间,把内存中的数据写入磁盘的临时文件,作为快照,恢复的时候把快照文件读进内存。如果宕机重启,那么内存里的数据肯定会没有的,那么再次启动redis后,则会恢复。

2. 备份与恢复

内存备份 –> 磁盘临时文件

临时文件 –> 恢复到内存

3. RDB优劣势

  • 优势
    1. 每隔一段时间备份,全量备份
    2. 灾备简单,可以远程传输
    3. 子进程备份的时候,主进程不会有任何io操作(不会有写入修改或删除),保证备份数据的的完整性
    4. 相对AOF来说,当有更大文件的时候可以快速重启恢复
  • 劣势
    1. 发生故障是,有可能会丢失最后一次的备份数据
    2. 子进程所占用的内存比会和父进程一模一样,如会造成CPU负担
    3. 由于定时全量备份是重量级操作,所以对于实时备份,就无法处理了。

4. RDB的配置

  1. 保存位置,可以在redis.conf自定义:

/user/local/redis/working/dump.rdb
2. 保存机制:

1
2
3
4
复制代码save 900 1
save 300 10
save 60 10000
save 10 3
1
2
3
markdown复制代码* 如果1个缓存更新,则15分钟后备份
* 如果10个缓存更新,则5分钟后备份
* 如果10000个缓存更新,则1分钟后备份
  1. stop-writes-on-bgsave-error
    • yes:如果save过程出错,则停止写操作
    • no:可能造成数据不一致
  2. rdbcompression
    • yes:开启rdb压缩模式
    • no:关闭,会节约cpu损耗,但是文件会大,道理同nginx
  3. rdbchecksum
    • yes:使用CRC64算法校验对rdb进行数据校验,有10%性能损耗
    • no:不校验

总结

RDB适合大量数据的恢复,但是数据的完整性和一致性可能会不足。

AOF

AOF特点

  1. 以日志的形式来记录用户请求的写操作。读操作不会记录,因为写操作才会存存储。
  2. 文件以追加的形式而不是修改的形式。
  3. redis的aof恢复其实就是把追加的文件从开始到结尾读取执行写操作。

优势

  1. AOF更加耐用,可以以秒级别为单位备份,如果发生问题,也只会丢失最后一秒的数据,大大增加了可靠性和数据完整性。所以AOF可以每秒备份一次,使用fsync操作。
  2. 以log日志形式追加,如果磁盘满了,会执行 redis-check-aof 工具
  3. 当数据太大的时候,redis可以在后台自动重写aof。当redis继续把日志追加到老的文件中去时,重写也是非常安全的,不会影响客户端的读写操作。
  4. AOF 日志包含的所有写操作,会更加便于redis的解析恢复。

劣势

  1. 相同的数据,同一份数据,AOF比RDB大
  2. 针对不同的同步机制,AOF会比RDB慢,因为AOF每秒都会备份做写操作,这样相对与RDB来说就略低。 每秒备份fsync没毛病,但是如果客户端的每次写入就做一次备份fsync的话,那么redis的性能就会下降。
  3. AOF发生过bug,就是数据恢复的时候数据不完整,这样显得AOF会比较脆弱,容易出现bug,因为AOF没有RDB那么简单,但是呢为了防止bug的产生,AOF就不会根据旧的指令去重构,而是根据当时缓存中存在的数据指令去做重构,这样就更加健壮和可靠了。

AOF的配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
csharp复制代码`# AOF 默认关闭,yes可以开启
appendonly no

# AOF 的文件名
appendfilename "appendonly.aof"

# no:不同步
# everysec:每秒备份,推荐使用
# always:每次操作都会备份,安全并且数据完整,但是慢性能差
appendfsync everysec

# 重写的时候是否要同步,no可以保证数据安全
no-appendfsync-on-rewrite no

# 重写机制:避免文件越来越大,自动优化压缩指令,会fork一个新的进程去完成重写动作,新进程里的内存数据会被重写,此时旧的aof文件不会被读取使用,类似rdb
# 当前AOF文件的大小是上次AOF大小的100% 并且文件体积达到64m,满足两者则触发重写
auto-aof-rewrite-percentage 100
auto-aof-rewrite-min-size 64mb`

到底采用RDB还是AOF呢?

  1. 如果你能接受一段时间的缓存丢失,那么可以使用RDB
  2. 如果你对实时性的数据比较care,那么就用AOF
  3. 使用RDB和AOF结合一起做持久化,RDB做冷备,可以在不同时期对不同版本做恢复,AOF做热备,保证数据仅仅只有1秒的损失。当AOF破损不可用了,那么再用RDB恢复,这样就做到了两者的相互结合,也就是说Redis恢复会先加载AOF,如果AOF有问题会再加载RDB,这样就达到冷热备份的目的了。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

阿里可观测性数据引擎的技术实践 一 前言 二 IT系统的可观

发表于 2021-11-25

简介:相比传统的告警、监控,可观测性能够以更加“白盒”的方式看透整个复杂的系统,帮助我们更好的观察系统的运行状况,快速定位和解决问题。就像发动机而言,告警只是告诉你发动机是否有问题,而一些包含转速、温度、压力的仪表盘能够帮我们大致确定是哪个部分可能有问题,而真正定位细节问题还需要观察每个部件的传感器数据才行。

作者 | 元乙

来源 | 阿里技术公众号

一 前言

可观测性这个概念最早出现于20世纪70年代的电气工程,核心的定义是:

A system is said to be observable if, for any possible evolution of state and control vectors, the current state can be estimated using only the information from outputs.

相比传统的告警、监控,可观测性能够以更加“白盒”的方式看透整个复杂的系统,帮助我们更好的观察系统的运行状况,快速定位和解决问题。就像发动机而言,告警只是告诉你发动机是否有问题,而一些包含转速、温度、压力的仪表盘能够帮我们大致确定是哪个部分可能有问题,而真正定位细节问题还需要观察每个部件的传感器数据才行。

二 IT系统的可观测性

电气化时代起源于第二次工业革命(Second Industrial Revolution)起于19世纪七十年代,主要标志是:电力、内燃机的广泛应用。而可观测性这一概念为何在近100年后才会被提出?难道在此之前就不需要依赖各类传感器的输出定位和排查故障和问题?显然不是,排查故障的方式一直都在,只是整个系统和情况更加复杂,所以才需要更加体系化、系统化的方式来支持这一过程,因此演化出来可观测性这个概念。所以核心点在于:

  • 系统更加的复杂:以前的汽车只需要一个发动机、传送带、车辆、刹车就可以跑起来,现在随便一个汽车上至少有上百个部件和系统,故障的定位难度变的更大。
  • 开发涉及更多的人:随着全球化时代的到来,公司、部分的分工也越来越细,也就意味着系统的开发和维护需要更多的部门和人来共同完成,协调的代价也越来越大。
  • 运行环境多种多样:不同的运行环境下,每个系统的工作情况是变化的,我们需要在任何阶段都能有效记录好系统的状态,便于我们分析问题、优化产品。

而IT系统经过几十年的飞速发展,整个开发模式、系统架构、部署模式、基础设施等也都经过了好几轮的优化,优化带来了更快的开发、部署效率,但随之而来整个的系统也更加的复杂、开发依赖更多的人和部门、部署模式和运行环境也更加动态和不确定,因此IT行业也已经到了需要更加系统化、体系化进行观测的这一过程。

IT系统的可观测性实施起来其实和电气工程还是比较类似,核心还是观察我们各个系统、应用的输出,通过数据来判断整体的工作状态。通常我们会把这些输出进行分类,总结为Traces、Metrics、Logs。关于这三种数据的特点、应用场景以及关系等,我们会在后面进行详细展开。

三 IT可观测性的演进

IT的可观测性技术一直在不断的发展中,从广义的角度上讲,可观测性相关的技术除了应用在IT运维的场景外,还可以应用在和公司相关的通用场景以及特殊场景中。

  1. IT运维场景:IT运维场景从横向、纵向来看,观察的目标从最基础的机房、网络等开始向用户的端上发展;观察的场景也从纯粹的错误、慢请求等发展为用户的实际产品体验。
  2. 通用场景:观测本质上是一个通用的行为,除了运维场景外,对于公司的安全、用户行为、运营增长、交易等都适用,我们可以针对这些场景构建例如攻击检测、攻击溯源、ABTest、广告效果分析等应用形式。
  3. 特殊场景:除了场景的公司内通用场景外,针对不同的行业属性,也可以衍生出特定行业的观测场景与应用,例如阿里云的城市大脑,就是通过观测道路拥堵、信号灯、交通事故等信息,通过控制不同红绿灯时间、出行规划等手段降低城市整体拥堵率。

四 Pragmatic可观测性如何落地

回到可观测性方案落地上,我们现阶段可能无法做出一个适用于各个行业属性的可观测引擎,更多的还是专注于DevOps和通用的公司商业方面。这里面的两个核心工作是:

  1. 数据的覆盖面足够的全:能够包括各类不同场景的不同类型的数据,除了狭义的日志、监控、Trace外,还需要包括我们的CMDB、变更数据、客户信息、订单/交易信息、网络流、API调用等
  2. 数据关联与统一分析:数据价值的发掘不是简单的通过一种数据来实现,更多的时候我们需要利用多种数据关联来达到目的,例如结合用户的信息表以及访问日志,我们可以分析不同年龄段、性别的用户的行为特点,针对性的进行推荐;通过登录日志、CMDB等,结合规则引擎,来实现安全类的攻击检测。

从整个流程来看,我们可以将可观测性的工作划分为4个组成部分:

  1. 传感器:获取数据的前提是要有足够传感器来产生数据,这些传感器在IT领域的形态有:SDK、埋点、外部探针等。
  2. 数据:传感器产生数据后,我们需要有足够的能力去获取、收集各种类型的数据,并把这些数据归类分析。
  3. 算力:可观测场景的核心是需要覆盖足够多的数据,数据一定是海量的,因此系统需要有足够的算力来计算和分析这些数据。
  4. 算法:可观测场景的终极应用是数据的价值发掘,因此需要使用到各类算法,包括一些基础的数值类算法、各种AIOps相关的算法以及这些算法的组合。

五 再论可观测性数据分类

  • Logs、Traces、Metrics作为IT可观测性数据的三剑客,基本可以满足各类监控、告警、分析、问题排查等需求,然而实际场景中,我们经常会搞混每种数据的适用形态,这里再大致罗列一下这三类数据的特点、转化方式以及适用场景:
  • Logs:我们对于Logs是更加宽泛的定义:记录事/物变化的载体,对于常见的访问日志、交易日志、内核日志等文本型以及包括GPS、音视频等泛型数据也包含在其中。日志在调用链场景结构化后其实可以转变为Trace,在进行聚合、降采样操作后会变成Metrics。
  • Metrics:是聚合后的数值,相对比较离散,一般有name、labels、time、values组成,Metrics数据量一般很小,相对成本更低,查询的速度比较快。
  • Traces:是最标准的调用日志,除了定义了调用的父子关系外(一般通过TraceID、SpanID、ParentSpanID),一般还会定义操作的服务、方法、属性、状态、耗时等详细信息,通过Trace能够代替一部分Logs的功能,通过Trace的聚合也能得到每个服务、方法的Metrics指标。

六 “割裂”的可观测性方案

业界也针对这种情况推出了各类可观察性相关的产品,包括开源、商业化的众多项目。例如:

  1. Metrics:Zabbix、Nagios、Prometheus、InfluxDB、OpenFalcon、OpenCensus
  2. Traces:Jaeger、Zipkin、SkyWalking、OpenTracing、OpenCensus
  3. Logs:ELK、Splunk、SumoLogic、Loki、Loggly

利用这些项目的组合或多或少可以解决针对性的一类或者几类问题,但真正应用起来你会发现各种问题:

  • 多套方案交织:可能要使用至少Metrics、Logging、Tracing3种方案,维护代价巨大
  • 数据不互通:虽然是同一个业务组件,同一个系统,产生的数据由于在不同的方案中,数据难以互通,无法充分发挥数据价值

在这种多套方案组合的场景下,问题排查需要和多套系统打交道,若这些系统归属不同的团队,还需要和多个团队进行交互才能解决问题,整体的维护和使用代价非常巨大。因此我们希望能够使用一套系统去解决所有类型可观测性数据的采集、存储、分析的功能。

七 可观测性数据引擎架构

基于上述我们的一些思考,回归到可观测这个问题的本质,我们目标的可观测性方案需要能够满足以下几点:

  1. 数据全面覆盖:包括各类的可观测数据以及支持从各个端、系统中采集数据
  2. 统一的系统:拒绝割裂,能够在一个系统中支持Traces、Metrics、Logs的统一存储与分析
  3. 数据可关联:每种数据内部可以互相关联,也支持跨数据类型的关联,能够用一套分析语言把各类数据进行融合分析
  4. 足够的算力:分布式、可扩展,面对PB级的数据,也能有足够的算力去分析
  5. 灵活智能的算法:除了基础的算法外,还应包括AIOps相关的异常检测、预测类的算法,并且支持对这些算法进行编排

可观测数据引擎的整体架构如下图所示,从底到上的四层也基本符合方案落地的指导思想:传感器+数据+算力+算法:

  • 传感器:数据源以OpenTelemetry为核心,并且支持各类数据形态、设备/端、数据格式的采集,覆盖面足够的“广”。
  • 数据+算力:采集上来的数据,首先会进入到我们的管道系统(类似于Kafka),根据不同的数据类型构建不同的索引,目前每天我们的平台会有几十PB的新数据写入并存储下。除了常见的查询和分析能力外,我们还内置了ETL的功能,负责对数据进行清洗和格式化,同时支持对接外部的流计算和离线计算系统。
  • 算法:除了基础的数值算法外,目前我们支持了十多种的异常检测/预测算法,并且还支持流式异常检测;同时也支持使用Scheduled SQL进行数据的编排,帮助我们产生更多新的数据。
  • 价值发掘:价值发掘过程主要通过可视化、告警、交互式分析等人机交互来实现,同时也提供了OpenAPI来对接外部系统或者供用户来实现一些自定义的功能。

八 数据源与协议兼容

随着阿里全面拥抱云原生后,我们也开始逐渐去兼容开源以及云原生的可观测领域的协议和方案。相比自有协议的封闭模式,兼容开源、标准协议大大扩充了我们平台能够支持的数据采集范围,而且减少了不必要的造轮子环节。上图展示了我们兼容外部协议、Agent的整体进度:

  • Traces:除了内部的飞天Trace、鹰眼Trace外,开源的包括Jaeger、OpenTracing、Zipkin、SkyWalking、OpenTelemetry、OpenCensus等。
  • Logs:Logs的协议较少,但是设计比较多的日志采集Agent,我们平台除了自研的Logtail外,还兼容包括Logstash、Beats(FileBeat、AuditBeat)、Fluentd、Fluent bits,同时还提供syslog协议,路由器交换机等可以直接用syslog协议上报数据到服务端。
  • Metrics:时序引擎我们在新版本设计之初就兼容了Prometheus,并且支持Telegraf、OpenFalcon、OpenTelemetry Metrics、Zabbix等数据接入。

九 统一存储引擎

对于存储引擎,我们的设计目标的第一要素是统一,能够用一套引擎存储各类可观测的数据;第二要素是快,包括写入、查询,能够适用于阿里内外部超大规模的场景(日写入几十PB)。

对于Logs、Traces、Metrics,其中Logs和Traces的格式和查询特点非常相似,我们放到一起来分析,推导的过程如下:

  • Logs/Traces:查询的方式主要是通过关键词/TraceID进行查询,另外会根据某些Tag进行过滤,例如hostname、region、app等每次查询的命中数相对较少,尤其是TraceID的查询方式,而且命中的数据极有可能是离散的通常这类数据最适合存储在搜索引擎中,其中最核心的技术是倒排索引
  • Metrics:通常都是range查询,每次查询某一个单一的指标/时间线,或者一组时间线进行聚合,例如统一某个应用所有机器的平均CPU时序类的查询一般QPS都较高(主要有很多告警规则),为了适应高QPS查询,需要把数据的聚合性做好对于这类数据都会有专门的时序引擎来支撑,目前主流的时序引擎基本上都是用类似于LSM Tree的思想来实现,以适应高吞吐的写入和查询(Update、Delete操作很少)

同时可观测性数据还有一些共性的特点,例如高吞吐写入(高流量、QPS,而且会有Burst)、超大规模查询特点、时间访问特性(冷热特性、访问局部性等)。

针对上述的特性分析,我们设计了一套统一的可观测数据存储引擎,整体架构如下:

  1. 接入层支持各类协议写入,写入的数据首先会进入到一个FIFO的管道中,类似于Kafka的MQ模型,并且支持数据消费,用来对接各类下游
  2. 在管道之上有两套索引结构,分别是倒排索引以及SortedTable,分别为Traces/Logs和Metrics提供快速的查询能力
  3. 两套索引除了结构不同外,其他各类机制都是共用的,例如存储引擎、FailOver逻辑、缓存策略、冷热数据分层策略等
  4. 上述这些数据都在同一个进程内实现,大大降低运维、部署代价
  5. 整个存储引擎基于纯分布式框架实现,支持横向扩展,单个Store最多支持日PB级的数据写入

十 统一分析引擎

如果把存储引擎比喻成新鲜的食材,那分析引擎就是处理这些食材的刀具,针对不同类型的食材,用不同种类的刀来处理才能得到最好的效果,例如蔬菜用切片刀、排骨用斩骨刀、水果用削皮刀等。同样针对不同类型的可观测数据和场景,也有对应的适合的分析方式:

  1. Metrics:通常用于告警和图形化展示,一般直接获取或者辅以简单的计算,例如PromQL、TSQL等
  2. Traces/Logs:最简单直接的方式是关键词的查询,包括TraceID查询也只是关键词查询的特例
  3. 数据分析(一般针对Traces、Logs):通常Traces、Logs还会用于数据分析和挖掘,所以要使用图灵完备的语言,一般程序员接受最广的是SQL

上述的分析方式都有对应的适用场景,我们很难用一种语法/语言去实现所有的功能并且具有非常好的便捷性(虽然通过扩展SQL可以实现类似PromQL、关键词查询的能力,但是写起来一个简单的PromQL算子可能要用一大串SQL才能实现),因此我们的分析引擎选择去兼容关键词查询、PromQL的语法。同时为了便于将各类可观测数据进行关联起来,我们在SQL的基础上,实现了可以连接关键词查询、PromQL、外部的DB、ML模型的能力,让SQL成为顶层分析语言,实现可观测数据的融合分析能力。

下面举几个我们的查询/分析的应用示例,前面3个相对比较简单,可以用纯粹的关键词查询、PromQL,也可以结合SQL一起使用。最后一个展示了实际场景中进行融合分析的例子:

  • 背景:线上发现有支付失败的错误,需要分析这些出现支付失败的错误的机器CPU指标有没有问题
  • 实现首先查询机器的CPU指标关联机器的Region信息(需要排查是否某个Region出现问题)和日志中出现支付失败的机器进行Join,只关心这些机器最后应用时序异常检测算法来快速的分析这些机器的CPU指标最后的结果使用线图进行可视化,结果展示更加直观

上述的例子同时查询了LogStore、MetricStore,而且关联CMDB以及ML模型,一个语句实现了非常复杂的分析效果,在实际的场景中还是经常出现的,尤其是分析一些比较复杂的应用和异常。

十一 数据编排

可观测性相比传统监控,更多的还是在于数据价值的发掘能力更强,能够仅通过输出来推断系统的运行状态,因此和数据挖掘这个工作比较像,收集各类繁杂的数据、格式化、预处理、分析、检验,最后根据得到的结论去“讲故事”。因此在可观测性引擎的建设上,我们非常关注数据编排的能力,能够让数据流转起来,从茫茫的原始日志中不断的去提取出价值更高的数据,最终告诉我们系统是否在工作以及为什么不工作。为了让数据能够“流转”起来,我们开发了几个功能:

  1. 数据加工:也就是大数据ETL(extract, transform, and load)中T的功能,能够帮我们把非结构化、半结构化的数据处理成结构化的数据,更加容易分析。
  2. Scheduled SQL:顾名思义,就是定期运行的SQL,核心思想是把庞大的数据精简化,更加利于查询,例如通过AccessLog每分钟定期计算网站的访问请求、按APP、Region粒度聚合CPU、内存指标、定期计算Trace拓扑等。
  3. AIOps巡检:针对时序数据特别开发的基于时序异常算法的巡检能力,用机器和算力帮我们去检查到底是哪个指标的哪个维度出现问题。

十二 可观测性引擎应用实践

目前我们这套平台上已经积累了10万级的内外部用户,每天写入的数据40PB+,非常多的团队在基于我们的引擎在构建自己公司/部门的可观测平台,进行全栈的可观测和业务创新。下面将介绍一些常见的使用我们引擎的场景:

1 全链路可观测

全链路的可观测性一直都是DevOps环节中的重要步骤,除了通常的监控、告警、问题排查外,还承担用户行为回放/分析、版本发布验证、A/B Test等功能,下图展示的是阿里内部某个产品内部的全链路可观测架构图:

  1. 数据源包括移动端、Web端、后端的各类数据,同时还包括一些监控系统的数据、第三方的数据等
  2. 采集通过SLS的Logtail和TLog实现
  3. 基于离在线混合的数据处理方式,对数据进行打标、过滤、关联、分发等预处理
  4. 各类数据全部存储在SLS可观测数据引擎中,主要利用SLS提供的索引、查询和聚合分析能力
  5. 上层基于SLS的接口构建全链路的数据展示和监控系统

2 成本可观测

商业公司的第一要务永远是营收、盈利,我们都知道盈利=营收-成本,IT部门的成本通常也会占据很大一个部分,尤其是互联网类型的公司。现在阿里全面云化后,包括阿里内部的团队也会在乎自己的IT支出,尽可能的压缩成本。下面的示例是我们阿里云上一家客户的监控系统架构,系统除了负责IT基础设施和业务的监控外,还会负责分析和优化整个公司的IT成本,主要收集的数据有:

  1. 收集云上每个产品(虚拟机、网络、存储、数据库、SaaS类等)的费用,包括详细的计费信息
  2. 收集每个产品的监控信息,包括用量、利用率等
  3. 建立起Catalog/CMDB,包括每个资源/实例所属的业务部门、团队、用途等

利用Catalog + 产品计费信息,就可以计算出每个部门的IT支出费用;再结合每个实例的用量、利用率信息,就可以计算出每个部门的IT资源利用率,例如每台ECS的CPU、内存使用率。最终计算出每个部门/团队整体上使用IT资源的合理度,将这些信息总结成运营报表,推动资源使用合理度低的部门/团队去优化。

3 Trace可观测

随着云原生、微服务逐渐在各个行业落地,分布式链路追踪(Trace)也开始被越来越多的公司采用。对于Trace而言,最基础的能力是能够记录请求在多个服务之间调用的传播、依赖关系并进行可视化。而从Trace本身的数据特点而言,它是规则化、标准化且带有依赖关系的访问日志,因此可以基于Trace去计算并挖掘更多的价值。

下面是SLS OpenTelemetry Trace的实现架构,核心是通过数据编排计算Trace原始数据并得到聚合数据,并基于SLS提供的接口实现各类Trace的附加功能。例如:

  1. 依赖关系:这是绝大部分的Trace系统都会附带的功能,基于Trace中的父子关系进行聚合计算,得到Trace Dependency
  2. 服务/接口黄金指标:Trace中记录了服务/接口的调用延迟、状态码等信息,基于这些数据可以计算出QPS、延迟、错误率等黄金指标。
  3. 上下游分析:基于计算的Dependency信息,按照某个Service进行聚合,统一Service依赖的上下游的指标
  4. 中间件分析:Trace中对于中间件(数据库/MQ等)的调用一般都会记录成一个个Span,基于这些Span的统计可以得到中间件的QPS、延迟、错误率。
  5. 告警相关:通常基于服务/接口的黄金指标设置监控和告警,也可以只关心整体服务入口的告警(一般对父Span为空的Span认为是服务入口调用)。

4 基于编排的根因分析

可观测性的前期阶段,很多工作都是需要人工来完成,我们最希望的还是能有一套自动化的系统,在出现问题的时候能够基于这些观测的数据自动进行异常的诊断、得到一个可靠的根因并能够根据诊断的根因进行自动的Fix。现阶段,自动异常恢复很难做到,但根因的定位通过一定的算法和编排手段还是可以实施的。

下图是一个典型的IT系统架构的观测抽象,每个APP都会有自己的黄金指标、业务的访问日志/错误日志、基础监控指标、调用中间件的指标、关联的中间件自身指标/日志,同时通过Trace还可以得到上下游APP/服务的依赖关系。通过这些数据再结合一些算法和编排手段就可以进行一定程度的自动化根因分析了。这里核心依赖的几点如下:

  1. 关联关系:通过Trace可以计算出APP/服务之间的依赖关系;通过CMDB信息可以得到APP和PaaS、IaaS之间的依赖关系。通过关联关系就可以“顺藤摸瓜”,找到出现问题的原因。
  2. 时序异常检测算法:自动检测某一条、某组曲线是否有异常,包括ARMA、KSigma、Time2Graph等,详细的算法可以参考:异常检测算法、流式异常检测。
  3. 日志聚类分析:将相似度高的日志聚合,提取共同的日志模式(Pattern),快速掌握日志全貌,同时利用Pattern的对比功能,对比正常/异常时间段的Pattern,快速找到日志中的异常。

时序、日志的异常分析能够帮我们确定某个组件是否存在问题,而关联关系能够让我们进行“顺藤摸瓜”。通过这三个核心功能的组合就可以编排出一个异常的根因分析系统。下图就是一个简单的示例:首先从告警开始分析入口的黄金指标,随后分析服务本身的数据、依赖的中间件指标、应用Pod/虚拟机指标,通过Trace Dependency可以递归分析下游依赖是否出现问题,其中还可以关联一些变更信息,以便快速定位是否由于变更引起的异常。最终发现的异常事件集中到时间轴上进行推导,也可以由运维/开发来最终确定根因。

十三 写在最后

可观测性这一概念并不是直接发明的“黑科技”,而是我们从监控、问题排查、预防等工作中逐渐“演化”出来的词。同样我们一开始只是做日志引擎(阿里云上的产品:日志服务),在随后才逐渐优化、升级为可观测性的引擎。对于“可观测性”我们要抛开概念/名词本身来发现它的本质,而这个本质往往是和商业(Business)相关,例如:

  1. 让系统更加稳定,用户体验更好
  2. 观察IT支出,消除不合理的使用,节省更多的成本
  3. 观察交易行为,找到刷单/作弊,即使止损
  4. 利用AIOps等自动化手段发现问题,节省更多的人力,运维提效

而我们对于可观测性引擎的研发,主要关注的也是如何服务更多的部门/公司进行可观测性方案的快速、有效实施。包括引擎中的传感器、数据、计算、算法等工作一直在不断进行演进和迭代,例如更加便捷的eBPF采集、更高压缩率的数据压缩算法、性能更高的并行计算、召回率更低的根因分析算法等。

原文链接

本文为阿里云原创内容,未经允许不得转载。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SpireDoc使用教程:在Java中隐藏Word中的特定

发表于 2021-11-25

​

Spire.Doc for Java是一款专业的Java Word组件,开发人员使用它可以轻松地将Word文档创建、读取、编辑、转换和打印等功能集成到自己的Java应用程序中。

在操作Word文档的过程中,有时需要对他人保密一些重要信息。因此,我们可以隐藏它们以确保机密性。本文展示了如何使用Spire.Doc for Java在 Word 文档中隐藏特定段落。可点击此处下载最新版测试。

在 Word 中隐藏特定段落

Spire.Doc for Java 支持使用TextRange.getCharacterFormat().setHidden(boolean value)方法隐藏 Word 中的特定段落。以下是要遵循的详细步骤。

  • 创建一个文档实例。
  • 使用Document.loadFromFile()方法加载示例 Word 文档。
  • 使用Document.getSections().get()方法获取Word 文档的特定部分。
  • 使用Section.getParagraphs().get()方法获取该部分的特定段落。
  • 循环遍历段落的子对象,如果是纯文本,则将每个子对象转换为文本范围。然后使用TextRange.getCharacterFormat().setHidden(boolean value)方法隐藏文本范围。
  • 使用Document.saveToFile()方法将文档保存到另一个文件。
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
typescript复制代码import com.spire.doc.*;
import com.spire.doc.documents.*;
import com.spire.doc.fields.*;

public class HideParagraph {
public static void main(String[] args) {
//Create a Document instance
Document document = new Document();

//Load a sample Word document
document.loadFromFile("C:\Users\Test1\Desktop\sample.docx");

//Get a specific section of Word
Section sec = document.getSections().get(0);

//Get a specific paragraph of the section
Paragraph para = sec.getParagraphs().get(1);

//Loop through the child objects
for (Object docObj : para.getChildObjects()) {
DocumentObject obj = (DocumentObject)docObj;

//Determine if a child object is an instance of TextRange
if ((obj instanceof TextRange)) {
TextRange range = ((TextRange)(obj));

//Hide the text range
range.getCharacterFormat().setHidden(true);
}
}

//Save the document to another file
document.saveToFile("output/hideParagraph.docx", FileFormat.Docx_2013);
}
}

Word格式处理控件Spire.Doc功能演示:在Java中隐藏Word中的特定段落​

整合所有格式API处理套包正在慧都网火热销售中!联系慧都客服立马1分钟了解全部咨询!

​

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

SQL去重的三种方法汇总

发表于 2021-11-25

在使用SQL提数的时候,常会遇到表内有重复值的时候,比如我们想得到 uv (独立访客),就需要做去重。

在 MySQL 中通常是使用 distinct 或 group by子句,但在支持窗口函数的 sql(如Hive SQL、Oracle等等) 中还可以使用 row_number 窗口函数进行去重。

举个栗子,现有这样一张表 task:

)

备注:

task_id: 任务id;

order_id: 订单id;

start_time: 开始时间

注意:一个任务对应多条订单

我们需要求出任务的总数量,因为 task_id 并非唯一的,所以需要去重:

distinct

1
2
3
4
5
6
7
sql复制代码-- 列出 task_id 的所有唯一值(去重后的记录)
-- select distinct task_id
-- from Task;

-- 任务总数
select count(distinct task_id) task_num
from Task;

distinct 通常效率较低。它不适合用来展示去重后具体的值,一般与 count 配合用来计算条数。

distinct 使用中,放在 select 后边,对后面所有的字段的值统一进行去重。比如distinct后面有两个字段,那么 1,1 和 1,2 这两条记录不是重复值 。

group by

1
2
3
4
5
6
7
8
9
10
csharp复制代码-- 列出 task_id 的所有唯一值(去重后的记录,null也是值)
-- select task_id
-- from Task
-- group by task_id;

-- 任务总数
select count(task_id) task_num
from (select task_id
from Task
group by task_id) tmp;

row_number

row_number 是窗口函数,语法如下:

row_number() over (partition by <用于分组的字段名> order by <用于组内排序的字段名>)

其中 partition by 部分可省略。

1
2
3
4
5
sql复制代码-- 在支持窗口函数的 sql 中使用
select count(case when rn=1 then task_id else null end) task_num
from (select task_id
, row_number() over (partition by task_id order by start_time) rn
from Task) tmp;

此外,再借助一个表 test 来理理 distinct 和 group by 在去重中的使用:

)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
sql复制代码-- 下方的分号;用来分隔行
select distinct user_id
from Test; -- 返回 1; 2

select distinct user_id, user_type
from Test; -- 返回1, 1; 1, 2; 2, 1

select user_id
from Test
group by user_id; -- 返回1; 2

select user_id, user_type
from Test
group by user_id, user_type; -- 返回1, 1; 1, 2; 2, 1

select user_id, user_type
from Test
group by user_id;
-- Hive、Oracle等会报错,mysql可以这样写。
-- 返回1, 1 或 1, 2 ; 2, 1(共两行)。只会对group by后面的字段去重,就是说最后返回的记录数等于上一段sql的记录数,即2条
-- 没有放在group by 后面但是在select中放了的字段,只会返回一条记录(好像通常是第一条,应该是没有规律的)

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

一文讲透gin中间件使用及源码解析 中间件使用介绍 原理解析

发表于 2021-11-25

这是我参与11月更文挑战的第10天,活动详情查看:2021最后一次更文挑战

pexels-charles-roth-2797318.jpg

在Gin框架中,中间件可谓是其精髓。一个个中间件组成一条中间件链,对HTTP Request请求进行拦截处理,实现了逻辑的解耦和分离。中间件之间互相独立,每个中间件只需要处理各自需要处理的事情即可。今天我们来详细地介绍Gin中间件的使用和原理。

中间件使用介绍

默认中间件

一般可以通过Gin提供的默认函数,来构建一个自带默认中间件的*Engine。

1
go复制代码r := gin.Default()

Default函数会默认设置两个系统中间件,即Logger 和 Recovery,实现打印日志输出和painc处理。

1
2
3
4
5
6
go复制代码func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery())
return engine
}

从第4行可以看到,Gin是通过Use方法设置中间件的,它接收一个可变参数,所以我们同时可以设置多个中间件。

1
go复制代码func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes

这时可以看到,一个Gin的中间件,其实就是Gin定义的一个HandlerFunc, 而它跟普通的处理器没有两样,比如:

1
2
3
4
go复制代码r.GET("/", func(c *gin.Context) {
fmt.Println("hello world")
c.JSON(200, "")
})

后面的func(c *gin.Context)这部分其实就是一个HandlerFunc。

自定义中间件

在上文中我们已经知道,Gin的中间件其实就是一个HandlerFunc, 那么我们只要实现一个HandlerFunc,就可以实现一个自定义的中间件。

现在假设我们要统计每次请求的执行时间,应该怎么定义这个中间件呢?

1
2
3
4
5
6
7
8
9
10
11
go复制代码func costTime() gin.HandlerFunc {
return func(c *gin.Context) {
//请求前获取当前时间
nowTime := time.Now()

//请求处理
c.Next()

log.Printf("the request URL %s cost %v", c.Request.URL.String(), time.Since(nowTime))
}
}

然后通过在服务初始化时使用该中间件。

1
2
3
4
5
6
7
8
9
10
11
go复制代码func main() {
r := gin.New()

r.Use(costTime()) // 使用自定义中间件

r.GET("/", func(c *gin.Context) {
c.JSON(200, "hello world")
})

r.Run(":8080")
}

效果示例如下:

1
vbscript复制代码the request URL / cost 1.003µs

原理解析

gin框架涉及中间件相关有4个常用的方法,它们分别是c.Next()、c.Abort()、c.Set()、c.Get()。

中间件的注册

首先看一下默认中间件的初始化实现流程:

1
2
3
4
5
6
go复制代码func Default() *Engine {
debugPrintWARNINGDefault()
engine := New()
engine.Use(Logger(), Recovery()) // 默认注册的两个中间件
return engine
}

继续往下查看一下Use()函数的代码:

1
2
3
4
5
6
go复制代码func (engine *Engine) Use(middleware ...HandlerFunc) IRoutes {
engine.RouterGroup.Use(middleware...) // 实际上还是调用的RouterGroup的Use函数
engine.rebuild404Handlers() // 系统其他插件
engine.rebuild405Handlers() // 系统其他插件
return engine
}

从下方的代码可以看出,注册中间件其实就是将中间件函数追加到group.Handlers中:

1
2
3
4
go复制代码func (group *RouterGroup) Use(middleware ...HandlerFunc) IRoutes {
group.Handlers = append(group.Handlers, middleware...)
return group.returnObj()
}

而我们注册路由时会将对应路由的函数和之前的中间件函数结合到一起:

1
2
3
4
5
6
go复制代码func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) IRoutes {
absolutePath := group.calculateAbsolutePath(relativePath)
handlers = group.combineHandlers(handlers) // 将处理请求的函数与中间件函数结合
group.engine.addRoute(httpMethod, absolutePath, handlers)
return group.returnObj()
}

其中结合操作的函数内容如下,注意观察这里是如何实现拼接两个切片得到一个新切片的。

1
2
3
4
5
6
7
8
9
10
11
12
go复制代码const abortIndex int8 = math.MaxInt8 / 2

func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain {
finalSize := len(group.Handlers) + len(handlers)
if finalSize >= int(abortIndex) { // 这里有一个最大限制
panic("too many handlers")
}
mergedHandlers := make(HandlersChain, finalSize)
copy(mergedHandlers, group.Handlers)
copy(mergedHandlers[len(group.Handlers):], handlers)
return mergedHandlers
}

也就是说,我们会将一个路由的中间件函数和处理函数结合到一起组成一条处理函数链条HandlersChain,而它本质上就是一个由HandlerFunc组成的切片:

1
go复制代码type HandlersChain []HandlerFunc

中间件的执行

我们在上面路由匹配的时候见过如下逻辑:

1
2
3
4
5
6
7
8
9
go复制代码value := root.getValue(rPath, c.Params, unescape)
if value.handlers != nil {
c.handlers = value.handlers
c.Params = value.params
c.fullPath = value.fullPath
c.Next() // 执行函数链条
c.writermem.WriteHeaderNow()
return
}

其中c.Next()就是很关键的一步,它的代码很简单:

1
2
3
4
5
6
7
go复制代码func (c *Context) Next() {
c.index++
for c.index < int8(len(c.handlers)) {
c.handlers[c.index](c)
c.index++
}
}

从上面的代码可以看到,这里通过索引遍历HandlersChain链条,从而实现依次调用该路由的每一个函数(中间件或处理请求的函数)。

gin_middleware1

我们可以在中间件函数中通过再次调用c.Next()实现嵌套调用(func1中调用func2;func2中调用func3),

gin_middleware2

或者通过调用c.Abort()中断整个调用链条,从当前函数返回。

1
2
3
go复制代码func (c *Context) Abort() {
c.index = abortIndex // 直接将索引置为最大限制值,从而退出循环
}

c.Set()/c.Get()

c.Set()和c.Get()这两个方法多用于在多个函数之间通过c传递数据的,比如我们可以在认证中间件中获取当前请求的相关信息(user信息等)通过c.Set()存入c,然后在后续处理业务逻辑的函数中通过c.Get()来获取当前请求的用户。c就像是一根管道,将该次请求相关的所有的函数都串起来了。

image-20211125105938030

总结

在本文中我们学习了gin中间件的使用及实现原理,也学习了如何自定义中间件,需要特别指出的是中间件在后端服务开发中有非常广泛的含义,大家如果感兴趣可以自行搜索,后续有机会我们再单独介绍后端中间件的使用和框架。还有一些gin中间件的高级实现,例如职责链模式,在特定的场景下非常有用和高效,大家也可以自行查阅相关资料。

参考资料

  1. www.flysnow.org/2020/06/28/…
  2. juejin.cn/post/698720…

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

CWE46标准中加入 OWASP 2021 TOP10

发表于 2021-11-25

​​摘要: 新发布的CWE4.6标准,加入了OWASP 2021 TOP10的视图。

本文分享自华为云社区《CWE 4.6 和 OWAPSTOP10(2021)》,作者:Uncle_Tom。

  1. CWE 4.6

CWE的更新,并没有因为疫情的影响,仍然保持着每年3-4个版本的更新速度,可见软件安全的重要性。这个月初4.6版本如约而至。我们来看下这次的更新,都有哪些亮点。

1.1 CWE V4.5 vs V4.6

通常可以从两个角度的对比CWE版本的变动,快速得到大致的差异信息。

1.1.1 CWE节点类型和数量的变动

每个CWE由一个唯一的编号表示一个弱点的分类节点,每个节点由他的类型确定它的节点种类,共有8种不同类型的节点类型。所以每种节点类型数量的变化可以直接反映这个版本的变化大小和变动方式。 每个版本的都有一个链接反映节点数量的变化。

先来看下两个版本CWE节点数量上的差异:

​从这个表的差异,不难看出:

  • 弱点(Weakness)增加了两个。目前CWE已经经过了15年左右的发展,基本上处于稳定状态,不太会有过多的CWE弱点类的节点增加;
  • 视图(View)增加了两个。视图是以不同的研究人员角度出发,对某些弱点的一个分类汇总的视图。在视图之下,通常是分类(Category)节点对某类弱点(Weakness)的一个分类。所以视图的增加,通常会伴随着分类(Category)节点的增加, 这次就增加了20个分类(Category)节点。

注,关于CWE节点的说明,请参看《话说CWE 4.2的新视图》。

1.1.2 从CWE XML schema变化看CWE 4.6的变动

CWE的所有信息是通过XML格式定义保存的,所以定义xml格式的schema文件xsd,就成了洞察CWE结构变化的最好方法。

每次CWE的版本,都会有一个链接来查看Schema的变化, 你也可以直接比较两个版本的Schema。

比较两个版本的schema文件,发现只有以下两个变动,如下图:

​检测方法枚举值(DetectionMethod Enumeration)的变动

  • 增加了”形式化验证(Formal Verification)”。形式化的方法已经开始逐步从学术走向了实际应用, 但形式化的方法依然会带来极高的验证成本;
  • 增加了”模拟/仿真(Simulation / Emulation)”。 通常这个方法用于硬件安全的验证。

有效性枚举值()的变动

  • 增加了”不鼓励的常见做法(Discouraged Common Practice)”。 也就是说这个安全消减措施的无效,不建议使用。避免了采取一些习惯性认为有效,实际上确无效的安全措施,防止习惯性思维导致的无效劳动。

1.1.3 新增的CWE节点

CWE-1341(重复释放同一个资源或句柄(Multiple Releases of Same Resource or Handle))

​如图,CWE-1341(重复释放同一个资源或句柄), 位于:CWE-710(编程规范违背) –> CWE-675(对资源的重复操作)下面。是我们熟悉的内存问题:CWE-415(双重释放)的上一级。 增加这个节点的原因,除了内存的双重释放,资源或文件句柄也存在着双重释放的问题。为了区分两种不同的场景,在细分上增加了这个新节点。

例如下面的代码,在第二次释放文件句柄的时候,会返回一个错误值。

1
2
3
4
5
6
7
8
9
10
11
12
13
ini复制代码char b[2000];
FILE *f = fopen("dbl_cls.c", "r");
if (f)
{
b[0] = 0;
fread(b, 1, sizeof(b) - 1, f);
printf("%s\n'", b);
int r1 = fclose(f);
printf("\n-----------------\n1 close done '%d'\n", r1);

int r2 = fclose(f); // Double close
printf("2 close done '%d'\n", r2);
}

​CWE-1342(瞬时执行后微架构状态造成信息泄漏(InformationExposure through Microarchitectural State after Transient Execution))

​处理器在不正确的微指令辅助(microcode assists)或预测执行(speculativeexecution)后,处理器没有正确清除微架构状态,从而导致信息泄漏。这种瞬时执行的痕迹可能会保留在微体系结构缓冲区中,从而导致微体系结构状态发生变化,攻击者可以使用侧信道分析获取敏感信息。例如,加载值注入 (LVI) REF-1202 可以利用将错误值直接注入中间加载和存储缓冲区。

注:这个问题属于CPU架构指令集和设计的问题,这个不是我的研究领域,跳过。

1.1.4 新增的视图

  • CWE-1343(2021最重要的硬件弱点(Weaknesses in the 2021 CWE MostImportant Hardware Weaknesses List))

这个不是我的研究领域,跳过。

  • CWE-1344(OWASP 2021 TOP10 弱点(Weaknesses in OWASP Top Ten (2021)))

这个的视图体现了OWASP 九月分发布的 OWASPTOP10 (2021)。 这个是OWASP 自2017 年之后,时隔四年,再次发布TOP10。接下来我们重点看下这个视图。

  1. CWE-1344 OWASP TOP10(2021)

Web应用程序安全性项目(Open Web Application Security Project (OWASP))是一个开放的社区, 该社区致力于使组织开发、购买和维护的应用程序和api,可以被信任。

OWASP已经先后在:2004,2007,2010,2013,2017,先后发布了OWASP TOP10,指出了Web应用软件存在的高危安全问题,并为每种漏洞按照其发生率、检测能力、影响和可利用性设定了一个优先级排名,从而帮助组织按照漏洞的优先级,关注、理解、正确识别、减轻在应用程序中这些漏洞造成的危害。同时也为检测工具厂商对这些高危的安全问题的检测提出了要求。

2.1 OWASP TOP10(2021)

2.2 OWASP2017和2021 TOP10的变化

OWASP2017和2021 TOP10的变化,如下图:

2.2.1 A01:2021-中断访问控制

从第五位上升到第一位。数据表明,平均而言,3.81%的受测应用程序具有一个或多个常见CWE,其中CWE在此风险类别中的出现次数超过31.8万次。映射到该问题的CWE多达34个,是映射CWE最多的一个分类。

2.2.2 A02:2021-加密故障

相较2017年上移一位,成为第二名,以前称为A3:2017-敏感数据泄露,敏感数据泄露是问题的表现,而不是根本原因。更新后的名称,侧重于故障根因与加密相关的问题。此类别通常会导致敏感数据泄露或系统泄露。

2.2.3 A03:2021-注入

从2017年的第一位,滑落到第三位。应用程序对此类问题的测试覆盖率达到94%,最大发生率为19%,平均发生率为3.37%,映射到这一类别的33个CWE在27.4万次的应用中发生率第二高。在此版本中,跨站脚本问题归于此类别的一个子类。

2.2.4 A04:2021-不安全的设计

这个是2021年的新类别,重点是与设计缺陷相关的风险。作为一个有”安全左移”追求的企业,需要更多的威胁建模、安全的设计模式和原则以及参考架构。

这里发散下,如果要从很大程度上解决软件的安全问题,安全左移的概念不仅仅是指检查的左移,而是更加向左,延伸到设计。这个也是上面提到的采用威胁建模、安全设计模式和原则,以及使用安全模块,提前的完成软件安全的纵深防御。Foritfy 的技术骨干Brian Chess 与 Jacob West写过一本书《用静态分析方法确保编程安全(SecureProgramming with Static Analysis)》,书中就提到“一半的安全问题都源自软件的设计,而非源码。”。我们必须意识到,在没有安全意识的设计人员开发出来的系统是非常的危险的。不安全的设计无法通过完美的实现来修复,安全起源于设计。

2.2.5 A05:2021-安全配置错误

从上一版的第六名上升到第五名。90%的应用程序都经过了某种形式的配置错误测试,平均发生率为 4.5%,并且有超过 20.8万次 CWE 映射到此风险类别。随着越来越多的人转向高度可配置的软件,这一类问题成上升趋势并不奇怪。在此版本中,A4:2017-XML 外部实体(XXE)被归为此类别的一个子类。

2.2.6 A06:2021-易受攻击和过时的组件

2017年为”使用具有已知漏洞的组件”,这一类别从2017年的第九位上升到第六位,是我们难以测试和评估风险的已知问题。它是唯一一个未将任何常见漏洞和披露(CVE) 映射到所包含的 CWE 的类别,因此漏洞利用和影响权重采用了默认值5.0。

2.2.7 A07:2021-标识和身份验证失败

2017是”身份验证中断”,从第二个位置向下滑动到第七位。现在包括与标识失败更相关的 CWE。这个类别仍然是前10名中不可或缺的一部分,标识和身份验证的标准化验证框架可用性的增强,似乎对这类问题的减少起到一定的帮助作用。

2.2.8 A08:2021-软件和数据完整性故障

2021年的新类别,侧重于在不验证完整性的情况下做出与软件更新、关键数据和 CI/CD 管道相关的假设。这类问题是CVE/CVSS影响度值最高的一个。在此版本中,A8:2017-不安全的反序列化并入到这个类别。

2.2.9 A09:2021-安全日志记录和监控故障

2017的A10:2017-日志记录和监控不足,从之前的第十位上升到第九位。此类别已扩展为包含更多类型的故障,测试具有挑战性,并且在 CVE/CVSS 数据中不能很好地表示。但是,此类别中的故障可能会直接影响可见性、事件警报和取证。

2.2.10 A10:2021-服务器端请求伪造

2021新增加的类型。数据显示,发生率相对较低,测试覆盖率高于平均水平,漏洞利用和影响潜力的评分高于平均水平。

  1. 参考

  • cwe.mitre.org/data/report…
  • cwe.mitre.org/data/defini…
  • owasp.org/Top10/
  • github.com/OWASP/Top10

点击关注,第一时间了解华为云新鲜技术~

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

RedisJson发布官方性能报告,性能碾压ES和Mongo

发表于 2021-11-25

一、概述

近期官网给出了RedisJson(RedisSearch)的性能测试报告,可谓碾压其他NoSQL,下面是核心的报告内容,先上结论:

  • 对于隔离写入(isolated writes),RedisJSON 比 MongoDB 快 5.4 倍,比 ElasticSearch 快 200 倍以上。
  • 对于隔离读取(isolated reads),RedisJSON 比 MongoDB 快 12.7 倍,比 ElasticSearch 快 500 倍以上。

在混合工作负载场景中,实时更新不会影响 RedisJSON 的搜索和读取性能,而 ElasticSearch 会受到影响。以下是具体的数据:

  • RedisJSON* 支持的操作数/秒比 MongoDB 高约 50 倍,比 ElasticSearch 高 7 倍/秒。
  • RedisJSON* 的延迟比 MongoDB 低约 90 倍,比 ElasticSearch 低 23.7 倍。

此外,RedisJSON 的读取、写入和负载搜索延迟在更高的百分位数中远比 ElasticSearch 和 MongoDB 稳定。当增加写入比率时,RedisJSON 还能处理越来越高的整体吞吐量,而当写入比率增加时,ElasticSearch 会降低它可以处理的整体吞吐量。

二、查询引擎

如前所述,reresearch和RedisJSON的开发非常强调性能。对于每一个版本,我们都想确保开发者可以体验到稳定和产品。为此,我们我们给出了一些分析工具、探测器来进行性能分析。

并且,我们每次发行新版本时时,也在不断的提升性能。特别是对于reresearch来说,2.2版本在加载和查询性能上都比2.0快了1.7倍,同时还改进了吞吐量和数据加载的延迟。

2.1 加载优化

接下来的两个图显示了运行纽约市出租车基准测试的运行结果(详细数据可以查看这里,该基准测试测量了吞吐量和加载耗时等基础数据。

在这里插入图片描述
在这里插入图片描述
从这些图表中可以看出,每一个reresearch的新版本都有一个实质性的性能改进。

2.2 全文搜索优化

为了评估搜索性能,我们索引了590万篇维基百科摘要。然后我们运行一个全文搜索查询面板,得到的结果如下图所示(详细信息在这里)。
在这里插入图片描述
在这里插入图片描述
从上面的图可以看出,通过从v2.0迁移到v2.2,同样的数据,在写、读、搜索(延迟图)方面都有了大幅度的改进,从而提高了运行Search和JSON的可实现吞吐量。

三、和其他框架的对比

为了评估RedisJSON的性能,我们决定将它与MongoDB和ElasticSearch进行比较。为了方便对比,我们会从文档存储、本地可用、云中可用、专业支持和提供可伸缩性、性能等方面进行全方位的对比。

我们使用了完善的YCSB标准来进行测试对比,它能够基于常见的工作负载来评估不同的产品,测量延迟、吞吐量曲线直到饱和。除了CRUD YCSB操作之外,我们还添加了一个两个字的搜索操作,专门帮助开发人员、系统架构师和DevOps从业者找到适合他们用例的最佳搜索引擎。

3.1 基准测试

此次测试,我们使用了如下的一些软件环境:

  • MongoDB v5.0.3
  • ElasticSearch 7.15
  • RedisJSON (RediSearch 2.2+RedisJSON 2.0)

此次是在Amazon Web Services 实例上运行基准测试,这三种解决方案都是分布式数据库,并且最常用于生产中的分布式方式。这就是为什么所有产品都使用相同的通用 m5d.8xlarge VM 和本地 SSD,并且每个设置由四个 VM 组成:一个客户端 + 三个数据库服务器。基准测试客户端和数据库服务器都在处于最佳网络条件下的单独 m5d.8xlarge 实例上运行,将实例紧密地打包在一个可用区内,实现稳态分析所需的低延迟和稳定的网络性能。

测试是在三节点集群上执行的,部署细节如下:

  • MongoDB 5.0.3:三成员副本集(Primary-Secondary-Secondary)。副本用于增加读取容量并允许更低的延迟读取。为了支持对字符串内容的文本搜索查询,在搜索字段上创建了一个文本索引。
  • ElasticSearch 7.15:15 个分片设置,启用查询缓存,并为 2 个基于 NVMe 的本地 SSD 提供 RAID 0 阵列,以实现更高级别的文件系统相关弹性操作性能。这 15 个分片为我们为 Elastic 所做的所有分片变体提供了可实现的最佳性能结果。
  • RedisJSON*: RediSearch 2.2 and RedisJSON 2.0: OSS Redis Cluster v6.2.6,有27个分片,均匀分布在三个节点上,加载了RediSearch 2.2和RedisJSON 2.0 OSS模块。

除了这个主要的基准/性能分析场景之外,我们还在网络、内存、CPU 和 I/O 上运行基准基准测试,以了解底层网络和虚拟机特性。在整个基准测试集期间,网络性能保持在带宽和 PPS 的测量限制以下,以产生稳定稳定的超低延迟网络传输(每个数据包 p99 < 100micros)。

接下来,我们将从提供单独的操作性能 [100% 写入] 和 [100% 读取] 开始,并以一组混合工作负载结束以模拟现实工作中的应用程序场景。

3.2 100% 写入基准

如下图所示,该基准测试表明,RedisJSON* 的摄取速度比 ElasticSearch 快 8.8 倍,比 MongoDB 快 1.8 倍,同时保持每个操作的亚毫秒级延迟。值得注意的是,99% 的 Redis 请求在不到 1.5 毫秒的时间内完成。

此外,RedisJSON* 是我们测试过的唯一一种在每次写入时自动更新其索引的解决方案。这意味着任何后续的搜索查询都会找到更新的文档。 ElasticSearch 没有这种细粒度的容量;它将摄取的文档放在一个内部队列中,并且该队列由服务器(不受客户端控制)每 N 个文档或每 M 秒刷新一次。他们称这种方法为近实时 (NRT)。 Apache Lucene 库(它实现了 ElasticSearch 的全文功能)旨在快速搜索,但索引过程复杂且繁重。如这些 WRITE 基准测试图表所示,由于这种“设计”限制,ElasticSearch 付出了巨大的代价。

结合延迟和吞吐量改进,RedisJSON* 比 Mongodb 快 5.4 倍,比 ElasticSearch 快 200 倍以上,用于隔离写入。
在这里插入图片描述
在这里插入图片描述

3.3 100% 读取基准

与写类似,我们可以观察到 Redis 在读取方面表现最佳,允许读取比 ElasticSearch 多 15.8 倍,比 MongoDB 多 2.8 倍,同时在整个延迟范围内保持亚毫秒级延迟,如下表所示。

在结合延迟和吞吐量改进时,RedisJSON* 比 MongoDB 快 12.7 倍,比 ElasticSearch 快 500 倍以上,用于隔离读取。

在这里插入图片描述
在这里插入图片描述

3.4 混合读/写/搜索基准

实际应用程序工作负载几乎总是读取、写入和搜索查询的混合。因此,在接近饱和时了解由此产生的混合工作负载吞吐量曲线更为重要。

作为起点,我们考虑了 65% 搜索和 35% 读取的场景,这代表了一个常见的现实世界场景,在该场景中,我们执行的搜索/查询比直接读取更多。65% 搜索、35% 读取和 0% 更新的初始组合也导致 ElasticSearch 和 RedisJSON* 的吞吐量相等。尽管如此,YCSB 工作负载允许您指定搜索/读取/更新之间的比率以满足您的要求。

“搜索性能”可以指不同类型的搜索,例如“匹配查询搜索”、“分面搜索”、“模糊搜索”等等。我们所做的最初向 YCSB 增加的搜索工作负载仅专注于“匹配查询搜索”,模仿分页的两词查询匹配,按数字字段排序。“匹配查询搜索”是任何启用搜索功能的供应商进行搜索分析的起点,因此,每个支持 YCSB 的数据库/驱动程序都应该能够在其基准驱动程序上轻松启用此功能。

在每个测试变体中,我们添加了 10% 的写入,以按相同的比例混合和减少搜索和读取百分比。这些测试变体的目标是了解每个产品如何处理数据的实时更新,我们认为这是事实上的架构目标,即写入立即提交到索引,读取始终是最新的。

在这里插入图片描述
正如您在图表中所看到的,在 RedisJSON* 上不断更新数据和增加写入比例不会影响读取或搜索性能并提高整体吞吐量。对数据产生的更新越多,对 ElasticSearch 性能的影响就越大,最终导致读取和搜索速度变慢。

ElasticSearch 可实现的 ops/sec 从 0% 更新到 50% 的演变,我们注意到它在 0% 更新基准上以 10k Ops/sec 开始,并受到严重影响,减少了 5 倍的 ops/sec,在50% 更新率基准。

与我们在上述单个操作基准中观察到的类似,MongoDB 搜索性能比 RedisJSON* 和 ElasticSearch 慢两个数量级,MongoDB 的最大总吞吐量为 424 ops/sec,而 RedisJSON* 为 16K 最大 ops/sec。

最后,对于混合工作负载,RedisJSON* 支持的操作数/秒比 MongoDB 高 50.8 倍,比 ElasticSearch 高 7 倍。如果我们将分析集中在混合工作负载期间的每种操作类型的延迟上,与 MongoDB 相比,RedisJSON* 可将延迟降低多达 91 倍,与 ElasticSearch 相比,延迟降低 23.7 倍。

3.5 完整延迟分析

与测量每个解决方案饱和之前产生的吞吐量曲线类似,在所有解决方案通用的可持续负载下进行完整的延迟分析也很重要。这将使您能够了解对于所有已发布操作在延迟方面最稳定的解决方案是什么,以及哪种解决方案不易受到应用程序逻辑引发的延迟峰值的影响(例如,弹性查询缓存未命中)。如果您想更深入地了解我们为什么要这样做,Gil Tene 提供了延迟测量注意事项的深入概述。

  • 查看上一节的吞吐量图表,并关注 10% 更新基准以包含所有三个操作,我们做了两种不同的可持续负载变化:
  • 250 ops/sec:比较 MongoDB、ElasticSearch 和 RedisJSON*,低于 MongoDB 的压力率。
    6000 ops/sec:比较 ElasticSearch 和 RedisJSON*,低于 ElasticSearch 压力率。

3.5.1 MongoDB 与 ElasticSearch 与 RedisJSON* 的延迟分析

在下面的第一张图片中,展示了从 p0 到 p9999 的百分位数,很明显,在每次搜索时,MongoDB 的表现都远远优于 Elastic 和 RedisJSON*。此外,关注 ElasticSearch 与 RedisJSON*,很明显,ElasticSearch 容易受到较高延迟的影响,这很可能是由垃圾收集 (GC) 触发器或搜索查询缓存未命中引起的。RedisJSON* 的 p99 低于 2.61 毫秒,而 ElasticSearch p999 搜索达到 10.28 毫秒。
在这里插入图片描述
在下面的读取和更新图表中,我们可以看到 RedisJSON* 在所有延迟范围内表现最佳,其次是 MongoDB 和 ElasticSearch。

RedisJSON* 是在所有分析的延迟百分位数上保持亚毫秒级延迟的唯一解决方案。在 p99,RedisJSON* 的延迟为 0.23 毫秒,其次是 MongoDB 的 5.01 毫秒和 ElasticSearch 的 10.49 毫秒。

在这里插入图片描述
在写入时,MongoDB 和 RedisJSON* 即使在 p99 时也能保持亚毫秒级的延迟。另一方面,ElasticSearch 显示出高尾延迟(> 10 毫秒),这很可能与导致 ElasticSearch 搜索峰值的原因 (GC) 相同。

在这里插入图片描述

3.5.2 ElasticSearch 与 RedisJSON 的延迟分析

仅关注 ElasticSearch 和 RedisJSON*,在保持 6K ops/sec 的可持续负载的同时,我们可以观察到 Elastic 和 RedisJSON* 的读取和更新模式与以 250 ops/sec 进行的分析保持一致。RedisJSON* 是更稳定的解决方案,其 p99 读取时间为 3 毫秒,而 Elastic 的 p99 读取时间为 162 毫秒。

在更新时,RedisJSON* 保留了 3 毫秒的 p99,而 ElasticSearch 则保留了 167 毫秒的 p99。

在这里插入图片描述
在这里插入图片描述
专注于搜索操作,ElasticSearch 和 RedisJSON* 以个位数 p50 延迟开始(p50 RedisJSON* 为 1.13 毫秒,而 ElasticSearch 的 p50 为 2.79 毫秒),其中 ElasticSearch 付出了 GC 触发和查询缓存未命中的代价在较高的百分位数上,在 >= p90 百分位数上清晰可见。

RedisJSON* 将 p99 保持在 33 毫秒以下,而 ElasticSearch 上的 p99 百分位数为 163 毫秒,高出 5 倍。

在这里插入图片描述

四、如何开始

开始使用RedisJSON*,我们可以创建一个免费的数据库在所有地区的Redis云,或者使用RedisJSON docker容器。我们已经更新了redisjson的文档,以方便开发者快速的开始使用查询和搜索功能。此外,正如我们在最近的客户机库声明中提到的,以下是几种流行语言的客户机驱动程序,可以帮助您快速入门。

RedisJSON*
Node.js node-redis
Java Jedis
.NET NRedisJSON NRediSearch
Python redis-py

参考:RedisJSON: Public Preview & Performance Benchmarking

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…200201202…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%