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

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


  • 首页

  • 归档

  • 搜索

【最完整系列】Redis-结构篇-快速列表

发表于 2020-01-29

注意:本系列文章分析的 Redis 源码版本:github.com/Sidfate/red…,是文章发布时间的最新版。

前景提要

在介绍快速列表之前,建议你要先了解下 ziplist 和 adlist,特别是 ziplist (参考我的文章《【最完整系列】Redis-结构篇-压缩列表》),关于 adlist 下面我会简单解释一下。

adlist

adlist 其实就是一个常规的双向链表实现,show you source code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码    typedef struct listNode {
struct listNode *prev;
struct listNode *next;
void *value;
} listNode;

typedef struct list {
listNode *head;
listNode *tail;
void *(*dup)(void *ptr);
void (*free)(void *ptr);
int (*match)(void *ptr, void *key);
unsigned long len;
} list;

如果你对上面的结构还很陌生不熟悉,可以在网上或者随便一本数据结构的书都可以找到,这里我也不再详细介绍了。

quicklist

ok,在我们切入正题前,先讲下在早期的 redis 版本中,列表键的实现分 2 种,元素数量少时用 ziplist,多时用 adlist,但在 3.2(网上资料查到的,不一定准确)之后的版本里,都用 quicklist 取代:

1
2
3
4
复制代码    > lpush test_list 1
(integer) 1
> object encoding test_list
"quicklist"

为什么要专门设计一个 quicklist 来重新定义呢?接下来从源码的角度来解读。首先还是看注释(redis 源码的注释真香):

quicklist.c - A doubly linked list of ziplists

然后再看下源码结构实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码    typedef struct quicklistNode {
struct quicklistNode *prev;
struct quicklistNode *next;
unsigned char *zl;
unsigned int sz; /* ziplist 占用的字节总数 */
unsigned int count : 16; /* ziplist 的元素个数 */
unsigned int encoding : 2; /* 是否被压缩,2表示被压缩,1表示原生 */
unsigned int container : 2;
unsigned int recompress : 1;
unsigned int attempted_compress : 1;
unsigned int extra : 10;
} quicklistNode;

typedef struct quicklist {
quicklistNode *head;
quicklistNode *tail;
unsigned long count; /* 所有 ziplists 的元素总和 */
unsigned long len; /* quicklistNodes 的个数 */
int fill : 16;
unsigned int compress : 16;
} quicklist;

字段的详细解释请参照本文末尾的字段详解部分。

所大家明白了吗,为什么我在一开始让大家先去了解下 ziplist 和 adlist,因为 quicklist 其实就是一个以 ziplist 为节点(quicklistNode 中存放指向 ziplist 的指针)的 adlist,没图说个JB:

知道结构后再回到我们最初的问题,quicklist 的结构为什么这样设计呢?quicklist 平衡了 ziplist 和 adlist 的优缺点:

  • 双向链表便于在表的两端进行 push 和 pop 操作,但是它的内存开销比较大。首先,它在每个节点上除了要保存数据之外,还要额外保存两个指针;其次,双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片,不利于内存管理。
  • ziplist 由于是一整块连续内存,所以存储效率很高。但是,它不利于修改操作,每次数据变动都会引发一次内存的 realloc。特别是当 ziplist 长度很长的时候,一次 realloc 可能会导致大批量的数据拷贝,进一步降低性能。

但是问题还是类了,quicklist 中 quicklistNode 包含多长的 ziplist 多少合适呢?长度如果小了,跟普通的双向链表也就差不多了,还是有内存碎片的问题;长度大了,每个 quicklist 节点上的 ziplist 需要大片的连续内存,操作内存的效率还是下降了。所以这个长度肯定是一个平衡值,它是 redis 提供的一个选项配置,默认是 -2,来看下官方说明:

1
2
3
4
5
6
7
8
9
10
复制代码# -5: max size: 64 Kb  <-- not recommended for normal workloads
# -4: max size: 32 Kb <-- not recommended
# -3: max size: 16 Kb <-- probably not recommended
# -2: max size: 8 Kb <-- good
# -1: max size: 4 Kb <-- good
# Positive numbers mean store up to _exactly_ that number of elements
# per list node.
# The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size),
# but if your use case is unique, adjust the settings as necessary.
list-max-ziplist-size -2

这里我就不翻译了,原生英文解释的很清楚了,唯一一点要说明下的是,当 list-max-ziplist-size 设置为正数时,表示每个 list 节点中储存元素个数。

压缩机制

大家仔细看 quicklist 的源码结构时,可能还注意到出现了很多 compress 的字样,这是因为 redis 为 quicklist 提供了一套压缩机制。

当 quicklist 很长的时候,最容易被访问的很可能是两端的数据,中间的数据被访问的频率比较低(访问起来性能也很低)。如果应用场景符合这个特点,redis 还提供了一个选项,能够把中间的数据节点进行压缩,从而进一步节省内存空间。Redis的配置参数 list-compress-depth 就是用来完成这个设置的。

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码# 列表也可以被压缩。
# 压缩深度指的是列表两侧开始不需要 ziplist 节点的深度(下面会解释)。
# 为了执行快速的 push/pop 操作,列表的头和尾通常不压缩。
# 设置如下:
# 0: 禁用压缩机制
# 1: 压缩深度 1 表示压缩除了头和尾之外的所有内部节点。例如结构:
# [head]->node->node->...->node->[tail]
# 因为[head], [tail]永远不会被压缩,它们直接的 node 都后被压缩。
# 2: [head]->[next]->node->node->...->node->[prev]->[tail]
# 2 表示不压缩 head,head->next,tail->prev 和 tail, 它们之前的 node 都压缩。
# 3: [head]->[next]->[next]->node->node->...->node->[prev]->[prev]->[tail]
# 以此类推...
list-compress-depth 0

这个参数默认是 0 也就是不压缩,Redis对于 quicklist 内部节点的压缩算法,采用的 LZF —— 一种无损压缩算法,有兴趣的可以看下 zh.wikipedia.org/wiki/LZFSE。

结构字段详解

之前的内容已经对源码结构中大多数的字段做了说明,但是还遗留一些字段我在这里统一解释下。

首先补充一个结构 quicklistLZF,后面的说明中会出现:

1
2
3
4
复制代码    typedef struct quicklistLZF {
unsigned int sz; /* LZF size in bytes*/
char compressed[];
} quicklistLZF;
属性 大小 含义
prev 8字节 指向链表前一个节点的指针。
next 8字节 指向链表后一个节点的指针。
zl 8字节 数据指针。如果当前节点的数据没有压缩,那么它指向一个 ziplist 结构;否则,它指向一个 quicklistLZF 结构。
sz 4字节 ziplist 占用的字节总数。如果指向的 ziplist 被压缩,仍然表示压缩前的字节总数。
count 16位 ziplist 包含的元素个数。
encoding 2位 表示ziplist是否压缩了。目前只有两种取值:2表示被压缩了(而且用的是LZF压缩算法),1表示没有压缩。
container 2位 数据容器。为1时表示 NONE,即一个 quicklist 节点下面直接存数据,为2时表示ZIPLIST,即使用ziplist存数据。
recompress 1位 bool值,当我们使用类似lindex这样的命令查看了某一项本来压缩的数据时,需要把数据暂时解压,这时就设置 recompress = 1 做一个标记,等有机会再把数据重新压缩。
attempted_compress 1位 bool值,在单元测试的时候用到。
extra 10位 扩展字段,以备后用。

需要注意的是,我发现网上的很多文章都没有提到的一点,官方给出的解释中说明了 quicklistNode 是一个 32 字节的结构,这应该是针对 64 位系统而言的,因为 prev,next 和 zl 都是指针,在 64 位系统中占 8 字节,以下的结构同理。

属性 大小 含义
head 8字节 指向头节点的指针。
tail 8字节 指向尾节点的指针。
count 8字节 所有 ziplists 的元素总个数。
len 8字节 quicklist 节点的个数。
fill 16位 ziplist大小设置,存放 list-max-ziplist-size 参数的值。
compress 16位 节点压缩深度,存放 list-compress-depth 参数的值。

另外需要注意一点的是 quicklist 结构在 64 位系统中是占 40 个字节,但是如上计算我得出的长度是 36 字节,这里面涉及到了结构体字节对齐约定,目的的话还是为了提升数据的读取效率。

属性 大小 含义
sz 4字节 压缩后的ziplist大小
compressed 待定 LZF 压缩后的数据

为什么 quicklistNode 中的 count 用 16 位就可以表示?

我们已经知道,ziplist 大小受到 list-max-ziplist-size 参数的限制。按照正值和负值有两种情况:

  • 当这个参数取正值的时候,就是恰好表示一个 quicklistNode 结构中 zl 所指向的 ziplist 所包含的数据项的最大值。list-max-ziplist-size 参数是由quicklist结构的 fill 字段来存储的,而 fill 字段是 16bit,所以它所能表达的值能够用 16bit 来表示。
  • 当这个参数取负值的时候,能够表示的 ziplist 最大长度是 64 Kb。而 ziplist 中每一个数据项,最少需要 2 个字节来表示(详见《【最完整系列】Redis-结构篇-压缩列表》)。所以,ziplist中数据项的个数不会超过 32 Kb,用 16bit 来表达足够了。

本文转载自: 掘金

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

【最完整系列】Redis-结构篇-压缩列表

发表于 2020-01-28

什么是压缩列表

压缩列表 ziplist 在 redis 中的应用也非常广泛,它是我们常用的 zset ,list 和 hash 结构的底层实现之一。当我们的容器对象的元素个数小于一定条件时,redis 会使用 ziplist 的方式储存,来减少内存的使用。

1
2
3
4
复制代码    > hset test_hash me sidfate
(integer) 1
> object encoding test_hash
"ziplist"

为什么要在元素较少的时候使用 ziplist ?

因为 redis 中的集合容器中,很多情况都用到了链表的实现,元素和元素之间通过储存的关联指针有序的串联起来,但是这样的指针往往是 随机I/O,也就是指针地址是不连续的(分布不均匀)。而我们的 ziplist 它本身是一块连续的内存块,所以它的读写是 顺序I/O,从底层的磁盘读写来说,顺序I/O 的效率肯定是高于 随机I/O 。你可能会问了,那为什么不都用 顺序I/O 的 ziplist 代替 随机I/O 呢,因为 ziplist 是连续内存,当你元素数量多了,意味着当你创建和扩展的时候需要操作更多的内存,所以 ziplist 针对元素少的时候才能提升效率。

ziplist 如何减少内存使用的呢?

接下来让我们从源码中一探究竟。

源码结构

题外话:每当你想要去探究一个项目的源码的时候,首先应该去看的就是它的注释,好的注释即是文档。同时也告诉我们平时开始也要注意注释的编写。

首先从源码的注释中我们可以了解一些基础信息:

ziplist 是经过特殊编码的双向列表结构,用来提高内存使用效率。它可以储存字符串或者整数值,其中整数值被编码成实际的整数,而不是字符串形式。它可以在 O(1) 时间内对列表的两端进行 push 和 pop 操作。但是,因为每个操作都需要重新分配 ziplist 使用的内存,所以实际的复杂度与 ziplist 使用的内存大小有关。

ziplist 结构的布局如下:

1
复制代码<zlbytes> <zltail> <zllen> <entry> <entry> ... <entry> <zlend>

属性 字节数 含义
zlbytes 4 压缩列表占用的内存字节数:在对压缩列表进行内存重分配, 或者计算 zlend 的位置时使用。
zltail 4 压缩列表表尾节点的偏移量:用来倒序遍历压缩列表。
zllen 2 记录了压缩列表包含的节点数量: 当这个属性的值小于 UINT16_MAX (65535)时, 这个属性的值就是压缩列表包含节点的数量; 当这个值等于 UINT16_MAX 时, 节点的真实数量需要遍历整个压缩列表才能计算得出。
entry[] 待定 节点数组,包含元素的具体信息
zlend 1 特殊值 0xFF (十进制 255 ),用于标记压缩列表的末端。

ziplist中的每个节点 entry 的结构如下:

1
复制代码<prevlen> <encoding> <entry-data>

redis 为了节约内存在 ziplist 的 entry 这个结构上有很多骚操作,让我来一一说明。

prevlen

prevlen 表示前一个元素的长度,以便能够从后向前遍历列表。它有一套特别的编码方式:如果这个长度小于254字节,那么它占用1个字节;当长度大于或等于254时,占用5个字节,第一个字节被设置为254 (0xFE),其余的4个字节采用前一个条目的长度作为值。prevlen 用 5 bytes表示时,不代表长度一定大于等于254,这是为了减少 realloc 和 memmove 提高效率。

为什么临界值是 254 ?我们来算一笔,一个字节最大能储存值为255,那临界值应该是255啊,别忘了我们还有个 zlend,它的值是0xFF(255),为了避免混淆,所以用254区分。

encoding

encoding 表示元素的编码,它取决于元素的内容。当元素是一个字符串时,编码的第一个字节的前2位将保存用于存储字符串长度的编码类型,然后是字符串的实际长度。当条目是整数时,前2位都设置为1。下面的2位用于指定在这个报头之后将存储哪种类型的整数。对不同类型和编码的概述如下。第一个字节总是足以确定条目的类型。

  • |00pppppp| - 1 字节

长度小于或等于63字节的字符串,63可以用6个字节表示,所以 pppppp 表示字符串的实际长度。

  • |01pppppp|qqqqqqqq| - 2 字节

长度小于或等于16383字节(14 位)的字符串。

  • |10000000|qqqqqqqq|rrrrrrrr|ssssssss|tttttttt| - 5 字节

长度大于16383(14位)的字符串,后4个字节代表长度。

  • |11000000| - 3 字节

11000000 + int16(2字节)。

  • |11010000| - 5 字节

11010000 + int32(4字节)。

  • |11100000| - 9 bytes

11010000 + int64(8字节)。

  • |11110000| - 4 bytes

11110000 + 24位有符号整数(3字节)。

  • |11111110| - 2 bytes

11110000 + int8(1字节)。

  • |1111xxxx|

极小整数,xxxx 的范围只能是 (00011101),也就是113,但是因为0000、1110、1111都被占用了。读取到的 value 需要将 xxxx 减 1,也就是整数 0~12 就是最终的 value。

  • |11111111|

表示 ziplist 的结束,也就是 zlend 的值 0xFF。

如果你觉得看的混乱了,别慌,上面的不需要全部记住,下面我会用一个鲜活的栗子(官方例子)来总结下。以下是一个包含了字符串 “2” 和 “5” 的压缩列表:

1
2
3
复制代码[0f 00 00 00] [0c 00 00 00] [02 00] [00 f3] [02 f6] [ff]
| | | | | |
zlbytes zltail zllen "2" "5" end

最前面的4个字节的代表着数字 0x0f = 15(zlbytes = 15),表示这个 ziplist 总共占用了15个字节。紧跟着的4个字节代表数字 0x0c = 12(zltail = 12),说明最后一个元素的偏移量是12,也就是 “5” 这个元素到 ziplist 开头的长度。接着是 zllen = 2,代表总共有2个元素。之后就是实际储存 “2” 和 “5” 的 entry。解读下 “2” 为什么是 00 f3,00表示前一个元素长度为0,因为它是第一个元素,f3 是 0x11110011,也就是我们的 1111xxxx encoding类型,3 - 1 = 2正好是我们的 “2”,“5” 也同理。
最后是 ff 结尾表示结束。

有没有童鞋注意到官方例子总我们储存的是字符串的 “2” 和 “5”,但是 redis 把它当成了整数来储存 ?这一点其实是 redis 故意做的,很多地方都会做类似处理,目的么,还是为了减少内存消耗。

最后我们再看一个储存字符串的例子,我们把上面的 “5” 换成 “Hello World”,那么原先的 “5” 这个 entry 会变成:

1
复制代码[02] [0b] [48 65 6c 6c 6f 20 57 6f 72 6c 64]

至于为什么的话,你们就对照的上面的自己试一遍,就当做练习题了。

本文转载自: 掘金

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

【最完整系列】Redis-结构篇-字典 Redis 字典

发表于 2020-01-26

Redis 字典

在 redis 中我们经常用到的 hash 结构,以及整个 redis 的 db 中 key-value 结构,都是以 dict 的形式存在,也就是字典。

源码结构

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
复制代码    // 字典结构
typedef struct dict {
// 类型特定函数
dictType *type;
// 保存类型特定函数需要使用的参数
void *privdata;
// 保存的两个哈希表,ht[0]是真正使用的,ht[1]会在rehash时使用
dictht ht[2];
// rehash进度,如果不等于-1,说明还在进行rehash
long rehashidx;
// 正在运行中的遍历器数量
unsigned long iterators;
} dict;

// hashtable结构
typedef struct dictht {
// 哈希表节点数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算哈希表的索引值,大小总是dictht.size - 1
unsigned long sizemask;
// 哈希表已经使用的节点数量
unsigned long used;
} dictht;

// hashtable的键值对节点结构
typedef struct dictEntry {
// 键名
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
// 指向下一个节点, 将多个哈希值相同的键值对连接起来
struct dictEntry *next;
} dictEntry;

由上面的结构我们可以看到 dict 结构内部包含两个 hashtable(以下简称ht),通常情况下只有一个 ht 是有值的。ht 是一个 dictht 的的结构,dictht 的结构和 Java 的 HashMap 几乎是一样的,都是通过分桶的方式解决 hash 冲突。第一维是数组,第二维是链表。数组中存储的是第二维链表的第一个元素的指针。这个指针在 ht 中就是指向一个 dictEntry 结构,里面存放着键值对的数据,以及指向下一个节点的指针。

Hash计算

Redis 计算哈希值和索引值的方法如下:

1
2
3
4
5
复制代码    // 使用字典设置的哈希函数,计算键 key 的哈希值
hash = dict->type->hashFunction(key);
// 使用哈希表的 sizemask 属性和哈希值,计算出索引值
// 根据情况不同, ht 可以是 ht[0] 或者 ht[1]
index = hash & dict->ht.sizemask;

hash函数我们这里就不说明了,计算出的 hash 值后将该值和 ht 的长度掩码(长度 - 1 )做与运算得出数组的索引值,这里我要解释下这么做的原因:

  1. 保证不会发生数组越界
    首先我们要知道,ht 中数组的长度按规定一定是2的幂(2的n次方)。因此,数组的长度的二进制形式是:10000…000,1后面有一堆0。那么,dict->ht.sizemask(dict->ht.size - 1) 的二进制形式就是01111…111,0后面有一堆1。最高位是0,和hash值相“与”,结果值一定不会比数组的长度值大,因此也就不会发生数组越界。
  2. 保证元素尽可能的均匀分布
    由上边的分析可知,dict->ht.size 一定是一个偶数,dict->ht.sizemask 一定是一个奇数。假设现在数组的长度(dict->ht.size)为16,减去1后(dict->ht.sizemask)就是15,15对应的二进制是:1111。现在假设有两个元素需要插入,一个哈希值是8,二进制是1000,一个哈希值是9,二进制是1001。和1111“与”运算后,结果分别是1000和1001,它们被分配在了数组的不同位置,这样,哈希的分布非常均匀。

那么,如果数组长度是奇数呢?减去1后(dict->ht.sizemask)就是偶数了,偶数对应的二进制最低位一定是 0,例如14二进制1110。对上面两个数子分别“与”运算,得到1000和1000。结果都是一样的值。那么,哈希值8和9的元素都被存储在数组同一个index位置的链表中。在操作的时候,链表中的元素越多,效率越低,因为要不停的对链表循环比较。

为什么 ht 中数组的长度一定是2的n次方?因为其实计算索引的过程其实就是取模(求余数),但是取余操作 % 的效率没有位运算 & 来的高,而 hash%length==hash&(length-1)的条件就是 length 是 2的次方,这里的原因上面也解释过了。

渐进式rehash

随着操作的不断执行, 哈希表保存的键值对会逐渐地增多或者减少, 为了让哈希表的负载因子(load factor)维持在一个合理的范围之内, 当哈希表保存的键值对数量太多或者太少时, 程序需要对哈希表的大小进行相应的扩展或者收缩,也就是 rehash。

Redis 对字典的哈希表执行 rehash 的步骤如下:

  1. 为字典的 ht[1] 哈希表分配空间, 这个哈希表的空间大小取决于要执行的操作, 以及 ht[0] 当前包含的键值对数量 (也即是 ht[0].used 属性的值):
    • 如果执行的是扩展操作, 那么 ht[1] 的大小为第一个大于等于 ht[0].used * 2 的 2^n (2 的 n 次方幂);
    • 如果执行的是收缩操作, 那么 ht[1] 的大小为第一个大于等于 ht[0].used 的 2^n 。
  2. 将保存在 ht[0] 中的所有键值对 rehash 到 ht[1] 上面: rehash 指的是重新计算键的哈希值和索引值, 然后将键值对放置到 ht[1] 哈希表的指定位置上。
  3. 当 ht[0] 包含的所有键值对都迁移到了 ht[1] 之后 (ht[0] 变为空表), 释放 ht[0] , 将 ht[1] 设置为 ht[0] , 并在 ht[1] 新创建一个空白哈希表, 为下一次 rehash 做准备。

这就是为什么redis 的 dict 中要保存2个 ht 的原因,方便2个 ht 的迁移替换。

为什么不直接复制 ht[0] 中的所有节点到 ht[1] 上而是 rehash 一遍?

我们在看一遍计算索引的公式:index = hash & dict->ht.sizemask;

注意到了吗,索引值的计算与字典数组的长度有关,而我们rehash时数组的长度是已经变化了,所以需要重新计算。

那么rehash的条件是什么呢,ht 达到什么样的数量redis会去执行rehash呢?

当以下条件中的任意一个被满足时, 程序会自动开始对哈希表执行扩展操作:

  1. 服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 1 ;
  2. 服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5 ;

其中哈希表的负载因子可以通过公式:

1
2
复制代码    // 负载因子 = 哈希表已保存节点数量 / 哈希表大小
load_factor = ht[0].used / ht[0].size

负载因子其实就是一个哈希表的使用比例,用来衡量哈希表的容量状态。

bgsave 或 bgrewriteaof 命令会造成内存页的过多分离 (Copy On Write),Redis 尽量不去扩容 ,但是如果 hash 表已经非常满了,元素的个数已经达到了第一维数组长度的 5 倍,这个时候就会强制扩容。

另一方面, 当哈希表的负载因子小于 0.1 时, 程序自动开始对哈希表执行收缩操作。

为什么称为渐进式?

扩展或收缩哈希表需要将 ht[0] 里面的所有键值对 rehash 到 ht[1] 里面, 可想而知大字典的 rehash过程是很耗时的,所以 redis 使用了一种渐进式的 rehash,也就是慢慢地将 ht[0] 里面的键值对 rehash 到 ht[1]。

以下是哈希表渐进式 rehash 的详细步骤:

  1. 为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
  2. 在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。
  3. 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一。
  4. 随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。

所以大家可以看到这整个过程是分步走的,每次rehash一点,直到执行完。那么问题也来的,在渐进式rehash的过程中,我们的字典里 ht[0] 和 ht[1] 会同时存在数据,那么这时候操作字典会不会混乱呢,redis为此提出了以下的逻辑判断:

因为在进行渐进式 rehash 的过程中, 字典会同时使用 ht[0] 和 ht[1] 两个哈希表, 所以在渐进式 rehash 进行期间, 字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行: 比如说, 要在字典里面查找一个键的话, 程序会先在 ht[0] 里面进行查找, 如果没找到的话, 就会继续到 ht[1] 里面进行查找, 诸如此类。

另外, 在渐进式 rehash 执行期间, 新添加到字典的键值对一律会被保存到 ht[1] 里面, 而 ht[0] 则不再进行任何添加操作: 这一措施保证了 ht[0] 包含的键值对数量会只减不增, 并随着 rehash 操作的执行而最终变成空表。

本文转载自: 掘金

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

Nodejs后端开发系列之sequelize零基础快速入门

发表于 2020-01-21

🎇🎇🎇新年快乐🎇🎇🎇

2020 鼠你最帅,
鼠你最强,
鼠你最棒,
鼠你最红,
鼠你最美,
鼠年吉祥

问与答

❓:你能学到什么?

🙋:sequelize-cli的使用以及sequelize的基础操作。

❓:为什么要使用sequelize-cli?
🙋:就像你使用Git/SVN来管理源代码的更改一样,你可以使用迁移来跟踪数据库的更改。

❓:为什么不将数据模型设计好再演示?

🙋:本文讲的是使用sequelize-cli和sequelize开发的过程。

❓:怎么会有这么多的代码?

🙋:每一步的代码我都贴了出来,只要按照流程做就能快速的完成一个示例。眼见为实,我相信这样学习的效果更好。

❓:怎么没有事务、作用域、数据类型等知识点?
🙋:这篇是入门教程,不过学会了这篇,事务、作用域理解起来更容易。

❓:为什么没有源代码?

🙋:做一遍一定比看一遍的效果好。

准备工作

1、初始化项目

1
2
复制代码 cd 工程目录 
npm init -y

2、安装模块

1
复制代码 npm i koa koa-body koa-router mysql2 sequelize sequelize-cli -S

3、添加server.js文件

1
2
3
4
5
6
7
8
9
10
11
12
复制代码 const Koa = require('koa');
const router = require('koa-router')();
const koaBody = require('koa-body');
const app = new Koa();
app.use(koaBody());

app.use(router.routes())
.use(router.allowedMethods('*'));

app.listen(3000, () => {
console.log('server is listening on 3000...')
});

快速入门

1、新建.sequelizerc文件

1
2
3
4
5
6
7
复制代码 const path = require('path');
module.exports = {
'config': path.resolve('config', 'config.json'), //数据库连接配置文件
'models-path': path.resolve('db', 'models'), //模型文件
'seeders-path': path.resolve('db', 'seeders'), //种子文件
'migrations-path': path.resolve('db', 'migrations') //迁移文件
}

2、初始化

1
复制代码 npx sequelize-cli init

3、编辑./db/config.js

1
2
3
4
5
6
7
8
复制代码 "development": {
"username": "username",
"password": "password",
"database": "school", //数据库名称
"host": "127.0.0.1",
"dialect": "mysql",
"timezone": "+08:00" //设置时区为'东八区'
}

4、创建数据库

1
复制代码 npx sequelize-cli db:create

5、生成student模型文件以及迁移文件

1
复制代码 npx sequelize-cli model:generate --name student --attributes student_name:string,student_age:integer,student_sex:boolean

6、编辑./db/migrations/xxxxx-create-student.js

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
复制代码 'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('student', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
student_name: {
type: Sequelize.STRING(10),
allowNull:false
},
student_age: {
type: Sequelize.INTEGER,
allowNull:false
},
student_sex: {
type: Sequelize.BOOLEAN,
allowNull:false
}
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('student');
}
};

打开xxxxx-create-student.你会发现createTable方法的第一个参数为students,这是由于sequelize会默认将表名称转换为复数形式,这里我将其修改为student,后面所有表名或模型名称都会使用单数形式。

7、生成名称为student的数据表

1
复制代码 npx sequelize-cli db:migrate

8、生成student表种子文件

1
复制代码 npx sequelize-cli seed:generate --name init-student

9、编辑./db/seeders/xxxxx-init-student.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码'use strict';

module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.bulkInsert('student', [{
student_name: '孙悟空',
student_age: 20,
student_sex: 1
},{
student_name: '白骨精',
student_age: 18,
student_sex: 0
},{
student_name: '猪八戒',
student_age: 16,
student_sex: 1
}])
},

down: (queryInterface, Sequelize) => {
return queryInterface.bulkDelete('student', null, {});
}
};

10、student表初始化数据

1
复制代码  npx sequelize-cli db:seed:all

11、编辑.db/models/student.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码'use strict';
module.exports = (sequelize, DataTypes) => {
const student = sequelize.define('student', {
student_name: DataTypes.STRING,
student_age: DataTypes.INTEGER,
student_sex: DataTypes.BOOLEAN,
class_id:DataTypes.INTEGER
}, {
timestamps: false,//不自动添加时间字段(updatedAt,createdAt)
freezeTableName: true,// 使用模型名称的单数形式
underscored: true //列名添加下划线
});
student.associate = function(models) {};
return student;
};

12、编辑server.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码.....
const Student = require('./db/models').student;
//添加学生信息
router.post('/student', async ctx => {
ctx.body = await Student.create(ctx.request.body);
});
//更新学生信息
router.put('/student', async ctx => {
const { id } = ctx.request.body;
ctx.body = await Student.update(ctx.request.body, { where: { id } });
});
//获取学生列表
router.get('/students', async ctx => {
ctx.body = await Student.findAll();
});
//根据id删除学生信息
router.delete('/student/:id', async ctx => {
const { id } = ctx.params;
ctx.body = await Student.destroy({ where: { id } });
});
.....

13、启动服务并使用Postman测试

1
复制代码 node server.js

模型关连

hasMany(一对多)

一个班级里面可以有多个学生,班级与学生的关系就是一对多。为了完成这个例子我们会做以下几件事情:

  1. 创建名称为_class的班级表
  2. _class班级表初始化数据
  3. student表添加列名为class_id的列
  4. 重新初始化student表数据
  5. 查询某个班级所有学生

让我们开始吧!

1、生成**_class**模型以及迁移文件

1
复制代码npx sequelize-cli model:generate --name _class --attributes class_name:string

2、修改./db/migrations/xxxxx-create-class.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码 'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('_class', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
class_name: {
type: Sequelize.STRING(10),
allowNull:false
}
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('_class');
}
};

3、生成**_class**表

1
复制代码npx sequelize-cli db:migrate

4、生成**_class**表种子文件

1
复制代码 npx sequelize-cli seed:generate --name init-class

5、编辑./db/seeders/xxxxx-init-class.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
复制代码'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.bulkInsert('_class', [{
class_name: '一班'
}, {
class_name: '二班'
}, {
class_name: '三班'
}]);
},

down: (queryInterface, Sequelize) => {
return queryInterface.bulkDelete('_class', null, {});
}
};

6、_class表初始化数据

1
复制代码  npx sequelize-cli db:seed  --seed  xxxxx-init-class.js

7、生成修改studnet表的迁移文件

1
复制代码npx sequelize-cli migration:generate  --name add-column-class_id-to-student.js

8、编辑./db/migrations/xxxxx-add-column-class_id-to-student.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码'use strict';

module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.addColumn('student', 'class_id', {
type: Sequelize.INTEGER,
allowNull:false
})
},

down: (queryInterface, Sequelize) => {
queryInterface.removeColumn('student', 'class_id', {});
}
};

9、修改student表

1
复制代码npx sequelize-cli db:migrate

10、重新生成student表种子文件

1
复制代码npx sequelize-cli seed:generate --name init-student-after-add-column-class_id

11、编辑./db/seeders/xxxxx-init-student-after-add-column-class_id.js文件

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
复制代码'use strict';

module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.bulkInsert('student', [{
student_name: '孙悟空',
student_age: 20,
student_sex: 1,
class_id: 1
}, {
student_name: '白骨精',
student_age: 18,
student_sex: 0,
class_id: 1
}, {
student_name: '猪八戒',
student_age: 16,
student_sex: 1,
class_id: 2
}, {
student_name: '唐僧',
student_age: 22,
student_sex: 1,
class_id: 1
}, {
student_name: '沙和尚',
student_age: 25,
student_sex: 1,
class_id: 1
}, {
student_name: '红孩儿',
student_age: 13,
student_sex: 1,
class_id: 2
}, {
student_name: '黑熊怪',
student_age: 26,
student_sex: 1,
class_id: 2
}, {
student_name: '太白金星',
student_age: 66,
student_sex: 1,
class_id: 3
}, {
student_name: '嫦娥',
student_age: 18,
student_sex: 0,
class_id: 3
}])
},

down: (queryInterface, Sequelize) => {
return queryInterface.bulkDelete('student', null, {});
}
};

12、撤销student表中已有的数据

1
复制代码npx sequelize-cli db:seed:undo --seed xxxxx-init-student.js

13、stuent表重新初始化数据

1
复制代码npx sequelize-cli db:seed --seed  xxxxx-init-student-after-add-column-class_id.js

14、编辑./db/models/_class.js文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码'use strict';
module.exports = (sequelize, DataTypes) => {
const _class = sequelize.define('_class', {
class_name: DataTypes.STRING
}, {
timestamps: false,
freezeTableName: true,
underscored: true
});
_class.associate = function (models) {
_class.hasMany(models.student);
};
return _class;
};

15、编辑server.js

1
2
3
4
5
6
7
8
9
复制代码...
const Class = require('./db/models')._class;

//获取班级信息以及班级里的所有学生
router.get('/classes', async ctx => {
//获取所有班级以及学生信息
ctx.body = await Class.findAll({ include: [Student] });
});
...

belongsTo(一对一)

一个学生只能属于一个班级,所以学生和班级的关系是一对一。

1、修改./db/models/student.js文件

1
2
3
4
5
复制代码 ...
student.associate = function(models) {
student.belongsTo(models._class); //一对一
};
...

2、修改server.js中获取学生列表的接口

1
2
3
4
5
6
复制代码...
//获取学生列表
router.get('/students', async ctx => {
ctx.body = await Student.findAll({ include: [Class] });
});
...

belongsTo VS hasOne

student.belongsTo(models._class)这里student叫做源模型,_class叫做目标模型。

student表中包含了_class表的外键class_id,也就是说外键在源模型上面所以我们使用belongsTo来创建关联。

hasOne和belongsTo都是用来创建一对一关联的,正确使用它们的方法就是看外键在哪个模型中。

  • belongsTo关联外键在源模型上
  • hasOne关联外键在目标模型上

belongsToMany(多对多)

一个班级可以有多名代课老师,一名代课老师可以带多个班级的课程。班级与老师的关系是多对多
为了完成此功能的演示,我们将做以下工作:

  1. 创建名称

让我们开始吧!

1、生成teacher模型以及迁移文件

1
复制代码npx sequelize-cli model:generate --name teacher --attributes teacher_name:string

2、修改./db/migrations/xxxxx-teacher-class.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
复制代码'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('teacher', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
teacher_name: {
type: Sequelize.STRING(10),
allowNull: false
}
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('teacher');
}
};

3、生成teacher表

1
复制代码npx sequelize-cli db:migrate

4、生成teacher表种子文件

1
复制代码 npx sequelize-cli seed:generate --name init-teacher

5、编辑./db/seeders/xxxxx-init-teacher.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
复制代码'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.bulkInsert('teacher', [{
teacher_name: '李老师'
}, {
teacher_name: '张老师'
}]);
},

down: (queryInterface, Sequelize) => {
return queryInterface.bulkDelete('teacher', null, {});
}
};

6、teacher表初始化数据

1
复制代码  npx sequelize-cli db:seed  --seed  xxxxx-init-teacher.js

7、生成teacher_class模型以及迁移文件

1
复制代码npx sequelize-cli model:generate --name teacher_class --attributes teacher_id:integer,class_id:integer

8、编辑./db/migrations/xxxxx-create-teacher-class.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
复制代码'use strict';
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.createTable('teacher_class', {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: Sequelize.INTEGER
},
teacher_id: {
type: Sequelize.INTEGER,
allowNull: false,
},
class_id: {
type: Sequelize.INTEGER,
allowNull: false,
}
});
},
down: (queryInterface, Sequelize) => {
return queryInterface.dropTable('teacher_class');
}
};

9、生成teacher_class表

1
复制代码npx sequelize-cli db:migrate

10、生成teacher_class表种子文件

1
复制代码 npx sequelize-cli seed:generate --name init-teacher_class

11、编辑./db/seeders/xxxxx-init-teacher_class.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
复制代码'use strict';

/* 李老师带的班级为一班和二班。张老师带的班级为三班 */
module.exports = {
up: (queryInterface, Sequelize) => {
return queryInterface.bulkInsert('teacher_class', [{
class_id: 1,
teacher_id: 1
}, {
class_id: 2,
teacher_id: 1
}, {
class_id: 3,
teacher_id: 2
}]);
},

down: (queryInterface, Sequelize) => {
return queryInterface.bulkDelete('teacher_class', null, {});
}
};

12、teacher_class表初始化数据

1
复制代码  npx sequelize-cli db:seed  --seed  xxxxx-init-teacher_class.js

13、编辑./db/models/teacher.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码'use strict';
module.exports = (sequelize, DataTypes) => {
const teacher = sequelize.define('teacher', {
teacher_name: DataTypes.STRING
}, {
timestamps: false,
freezeTableName: true,
underscored: true
});
teacher.associate = function (models) {
teacher.belongsToMany(models._class, {
through: models.teacher_class,
foreignKey: 'teacher_id',
});
};
return teacher;
};

14、编辑./db/models/_class.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
复制代码'use strict';
module.exports = (sequelize, DataTypes) => {
const _class = sequelize.define('_class', {
class_name: DataTypes.STRING
}, {
timestamps: false,
freezeTableName: true,
underscored: true
});
_class.associate = function (models) {
_class.hasMany(models.student);
_class.belongsToMany(models.teacher, {
through: models.teacher_class,
foreignKey: 'class_id',
});
};
return _class;
};

15、编辑server.js

1
2
3
4
5
6
7
复制代码const Teacher = require('./db/models').teacher;

//获取老师信息以及老师所带的班级
router.get('/teachers', async ctx => {
//获取所有班级以及学生信息
ctx.body = await Teacher.findAll({ include: [Class] });
})

查询

基础查询

1、返回指定列

1
2
3
4
复制代码 Student.findAll({
attributes: ['id', 'student_name']
});
// select id,student_name from student

2、单条件查询

1
2
3
4
5
6
复制代码 Student.findAll({
where: {
id: 1
}
})
// select * from student where id = 1

3、AND

1
2
3
4
5
6
7
8
复制代码 //返回id为1,姓名是`孙悟空`的学生信息
Student.findAll({
where: {
id: 4,
student_name:'孙悟空'
}
})
// select * from student where id = 1 and student_name = '孙悟空'

4、OR

1
2
3
4
5
6
7
8
9
复制代码 //返回年龄等于12或者22的学生信息
Student.findAll({
where: {
student_age: {
[Op.or]: [12, 22]
}
}
})
// select * from student where studnet_age = 12 or studnet_age = 22

5、条件查询- >,>=,<,<=,=

1
2
3
4
5
6
7
8
9
复制代码 // 返回年龄大于等于20的学生
Student.findAll({
where: {
student_age: {
[Op.gte]: 20
}
}
})
// select * from student where studnet_age >= 20
1
2
3
4
5
6
复制代码 [Op.gt]: 6      //大于6
[Op.gte]: 6 //大于等于6
[Op.lt]: 10 //小于10
[Op.lte]: 10 //小于等于10
[Op.ne]: 20 //不等于20
[Op.eq]: 3 //等于3

6、IN

1
2
3
4
5
6
7
8
9
复制代码 // 返回年龄是16和18的学生信息
Student.findAll({
where: {
student_age: {
[Op.in]: [16,18]
}
}
})
// select * from student where studnet_age in (16,18)

7、LIKE

1
2
3
4
5
6
7
8
9
复制代码  // 返回名称包含'孙'的学生信息
Student.findAll({
where: {
student_name: {
[Op.like]: '%孙%',
}
}
})
// select * from student where studnet_name like '%孙%'

聚合函数

1、获取学生的平均年龄

1
2
3
复制代码 Student.findAll({
attributes: [[sequelize.fn('AVG', sequelize.col('student_age')), 'avg']]
})

2、获取学生总数

1
2
3
复制代码  Student.findAll({
attributes: [[sequelize.fn('COUNT', sequelize.col('id')), 'count']]
})

嵌套查询

1、获取一班所有的学生并根据年龄降序排列

1
2
3
4
5
复制代码 Class.findAll({
include: [{model: Student}],
where:{id:1},
order:[[Student,'student_age', 'DESC']]
});

参考

sequelize中文文档

本文转载自: 掘金

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

Dubbo 275在线程模型上的优化 本文目录 官方发布

发表于 2020-01-20

这是why技术的第30篇原创文章

这可能是全网第一篇解析Dubbo 2.7.5里程碑版本中的改进点之一:客户端线程模型优化的文章。

先劝退:文本共计8190字,54张图。阅读之前需要对Dubbo相关知识点有一定的基础。内容比较硬核,劝君谨慎阅读。


读不下去不要紧,我写的真的很辛苦的,帮忙拉到最后点个赞吧。

本文目录

第一节:官方发布

本小节主要是通过官方发布的一篇名为《Dubbo 发布里程碑版本,性能提升30%》的文章作为引子,引出本文所要分享的内容:客户端线程模型优化。

第二节:官网上的介绍

在介绍优化后的消费端线程模型之前,先简单的介绍一下Dubbo的线程模型是什么。同时发现官方文档对于该部分的介绍十分简略,所以结合代码对其进行补充说明。

第三节:2.7.5版本之前的线程模型的问题

通过一个issue串联本小节,道出并分析一些消费端应用,当面临需要消费大量服务且并发数比较大的大流量场景时(典型如网关类场景),经常会出现消费端线程数分配过多的问题。

第四节:threadless是什么

通过第三节引出了新版本的解决方案,threadless。并对其进行一个简单的介绍。

第五节:场景复现

由于条件有限,场景复现起来比较麻烦,但是我在issues#890中发现了一个很好的终结,所以我搬过来了。

第六节:新旧线程模型对比

本小节通过对比新老线程模型的调用流程,并对比2.7.4.1版本和2.7.5版本关键的代码,起到一个导读的作用。

第七节:Dubbo版本介绍。

趁着这次的版本升级,也趁机介绍一下Dubbo目前的两个主要版本:2.6.X和2.7.X。

官方发布

2020年1月9日,阿里巴巴中间件发布名为《Dubbo 发布里程碑版本,性能提升30%》的文章:


文章中说这是Dubbo的一个里程碑式的版本。

在阅读了相关内容后,我发现这确实是一个里程碑式的跨域,对于Dubbo坎坷的一生来说,这是展现其强大的生命力和积极探索精神的一个版本。

强大的生命力体现在新版本发布后众多的或赞扬、或吐槽的社区反馈。

探索精神体现在Dubbo在多语言和协议穿透性上的探索。

在文章中列举了9大改造点,本文仅介绍2.7.5版本中的一个改造点:优化后的消费端线程模型。

本文大部分源码为2.7.5版本,同时也会有2.7.4.1版本的源码作为对比。

官网上的介绍

在介绍优化后的消费端线程模型之前,先简单的介绍一下Dubbo的线程模型是什么。

直接看官方文档中的描述,Dubbo官方文档是一份非常不错的入门学习的文档,很多知识点都写的非常详细。

可惜,在线程模型这块,差强人意,寥寥数语,图不达意:


官方的配图中,完全没有体现出线程”池”的概念,也没有体现出同步转异步的调用链路。仅仅是一个远程调用请求的发送与接收过程,至于响应的发送与接收过程,这张图中也没有表现出来。

所以我结合官方文档和2.7.5版本的源码进行一个简要的介绍,在阅读源码的过程中你会发现:

在客户端,除了用户线程外,还会有一个线程名称为DubboClientHandler-ip:port的线程池,其默认实现是cache线程池。


上图的第93行代码的含义是,当客户端没有指定threadpool时,采用cached实现方式。

上图中的setThreadName方法,就是设置线程名称:

org.apache.dubbo.common.utils.ExecutorUtil#setThreadName


可以清楚的看到,线程名称如果没有指定时,默认是DubboClientHandler-ip:port。

在服务端,除了有boss线程、worker线程(io线程),还有一个线程名称为DubboServerHandler-ip:port的线程池,其默认实现是fixed线程池。


启用线程池的dubbo.xml配置如下:

<dubbo:protocol name="dubbo" threadpool="xxx"/>

上面的xxx可以是fixed、cached、limited、eager,其中fixed是默认实现。当然由于是SPI,所以也可以自行扩展:


所以,基于最新2.7.5版本,官方文档下面红框框起来的这个地方,描述的有误导性:


从SPI接口看来,fixed确实是缺省值。

但是由于客户端在初始化线程池之前,加了一行代码(之前说的93行),所以客户端的默认实现是cached,服务端的默认实现是fixed。

我也看了之前的版本,至少在2.6.0时(更早之前的版本没有查看),客户端的线程池的默认实现就是cached。

关于Dispatcher部分的描述是没有问题的:


Dispatcher部分是线程模型中一个比较重要的点,后面会提到。

这里配一个稍微详细一点的2.7.5版本之前的线程模型,供大家参考:


图片来源:https://github.com/apache/dubbo/issues/890

2.7.5之前的线程模型的问题

那么改进之前的线程模型到底存在什么样的问题呢?

在《Dubbo 发布里程碑版本,性能提升30%》一文中,是这样描述的:

对 2.7.5 版本之前的 Dubbo 应用,尤其是一些消费端应用,当面临需要消费大量服务且并发数比较大的大流量场景时(典型如网关类场景),经常会出现消费端线程数分配过多的问题。

同时文章给出了一个issue的链接:

https://github.com/apache/dubbo/issues/2013

这一小节,我就顺着这个issue#2013给大家捋一下Dubbo 2.7.5版本之前的线程模型存在的问题,准确的说,是客户端线程模型存在的问题:


首先,Jaskey说到,分析了issue#1932,他说在某些情况下,会创建非常多的线程,因此进程会出现OOM的问题。

在分析了这个问题之后,他发现客户端使用了一个缓存线程池(就是我们前面说的客户端线程实现方式是cached),它并没有限制线程大小,这是根本原因。

接下来,我们去issue#1932看看是怎么说的:

https://github.com/apache/dubbo/issues/1932


可以看到issue#1932也是Jaskey提出的,他主要传达了一个意思:为什么我设置了actives=20,但是在客户端却有超过10000个线程名称为DubboClientHandler的线程的状态为blocked?这是不是一个Bug呢?

仅就这个issue,我先回答一下这个:不是Bug!

我们先看看actives=20的含义是什么:


按照官网上的解释:actives=20的含义是每个服务消费者每个方法最大并发调用数为20。

也就是说,服务端提供一个方法,客户端调用该方法,同时最多允许20个请求调用,但是客户端的线程模型是cached,接受到请求后,可以把请求都缓存到线程池中去。所以在大量的比较耗时的请求的场景下,客户端的线程数远远超过20。

这个actives配置在《一文讲透Dubbo负载均衡之最小活跃数算法》这篇文章中也有说明。它的生效需要配合ActiveLimitFilter过滤器,actives的默认值为0,表示不限制。当actives>0时,ActiveLimitFilter自动生效。由于不是本文重点,就不在这里详细说明了,有兴趣的可以阅读之前的文章。

顺着issue#2013捋下去,我们可以看到issue#1896提到的这个问题:


问题1我已经在前面解释了,他这里的猜测前半句对,后半句错。不再多说。

这里主要看问题2(可以点开大图看看):服务提供者多了,消费端维护的线程池就多了。导致虽然服务提供者的能力大了,但是消费端有了巨大的线程消耗。他和下面issue#4467的哥们表达的是同一个意思:想要的是一个共享的线程池。

我们接着往下捋,可以发现issue#4467和issue#5490


对于issue#4467,CodingSinger说:为什么Dubbo对每一个链接都创建一个线程池?


从Dubbo 2.7.4.1的源码我们也可以看到确实是在WarppedChannelHandler构造函数里面确实是为每一个连接都创建了一个线程池:


issue#4467想要表达的是什么意思呢?

就是这个地方为什么要做链接级别的线程隔离,一个客户端,就算有多个连接都应该用共享线程池呀?

我个人也觉得这个地方不应该做线程隔离。线程隔离的使用场景应该是针对一些特别重要的方法或者特别慢的方法或者功能差异较大的方法。很显然,Dubbo的客户端就算一个方法有多个连接(配置了connections参数),也是一视同仁,不太符合线程隔离的使用场景。

然后chickenij大佬在2019年7月24日回复了这个issue:


现有的设计就是:provider端默认共用一个线程池。consumer端是每个链接共享一个线程池。

同时他也说了:对于consumer线程池,当前正在尝试优化中。

言外之意是他也觉得现有的consumer端的线程模型也是有优化空间的。

这里插一句:chickenlj是谁呢?

刘军,GitHub账号Chickenlj,Apache Dubbo PMC,项目核心维护者,见证了Dubbo从重启开源到Apache毕业的整个流程。现任职阿里云云原生应用平台团队,参与服务框架、微服务相关工作,目前主要在推动Dubbo开源的云原生化。


他这篇文章的作者呀,他的话还是很有分量的。

之前也在Dubbo开发者日成都站听到过他的分享:


如果对他演讲的内容有兴趣的朋友可以在公众号的后台回复:1026。领取讲师PPT和录播地址。

好了,我们接着往下看之前提到的issue#5490,刘军大佬在2019年12月16日就说了,在2.7.5版本时会引入threadless executor机制,用于优化、增强客户端线程模型。


threadless是什么?
==============


根据类上的说明我们可以知道:

这个Executor和其他正常Executor之间最重要的区别是这个Executor不管理任何线程。

通过execute(Runnable)方法提交给这个执行器的任务不会被调度到特定线程,而其他的Executor就把Runnable交给线程去执行了。

这些任务存储在阻塞队列中,只有当thead调用waitAndDrain()方法时才会真正执行。简单来说就是,执行task的thead与调用waitAndDrain()方法的thead完全相同。

其中说到的waitAndDrain()方法如下:


execute(Runnable)方法如下:


同时我们还可以看到,里面还维护了一个名称叫做sharedExecutor的线程池。见名知意,我们就知道了,这里应该是要做线程池共享了。

场景复现

上面说了这么多2.7.5版本之前的线程模型的问题,我们怎么复现一次呢?

我这里条件有限,场景复现起来比较麻烦,但是我在issues#890中发现了一个很好的终结,我搬过来即可:


根据他接下来的描述做出思维导图如下:


上面说的是corethreads大于0的场景。但是根据现有的线程模型,即使核心池数(corethreads)为0,当消费者应用依赖的服务提供者处理很慢时且请求并发量比较大时,也会出现消费者线程数很多问题。大家可以对比着看一下。

新旧线程模型对比

在之前的介绍中大家已经知道了,这次升级主要是增强客户端线程模型,所以关于2.7.5版本之前和之后的线程池模型我们主要关心Consumer部分。

老的线程模型

老的线程池模型如下,注意线条颜色:


1、业务线程发出请求,拿到一个 Future 实例。

2、业务线程紧接着调用 future.get 阻塞等待业务结果返回。
3、当业务数据返回后,交由独立的 Consumer 端线程池进行反序列化等处理,并调用 future.set 将反序列化后的业务结果置回。
4、业务线程拿到结果直接返回。

新的线程模型

新的线程池模型如下,注意线条颜色:


1、业务线程发出请求,拿到一个 Future 实例。
2、在调用 future.get() 之前,先调用 ThreadlessExecutor.wait(),wait 会使业务线程在一个阻塞队列上等待,直到队列中被加入元素。
3、当业务数据返回后,生成一个 Runnable Task 并放ThreadlessExecutor 队列。
4、业务线程将 Task 取出并在本线程中执行反序列化业务数据并 set 到 Future。
5、业务线程拿到结果直接返回。

可以看到,相比于老的线程池模型,新的线程模型由业务线程自己负责监测并解析返回结果,免去了额外的消费端线程池开销。

代码对比

接下来我们对比一下2.7.4.1版本和2.7.5版本的代码,来说明上面的变化。

需要注意的是,由于涉及到的变化代码非常的多,我这里仅仅起到一个导读的作用,如果读者想要详细了解相关变化,还需要自己仔细阅读源码。

首先两个版本的第一步是一样的:业务线程发出请求,拿到一个Future实例。

但是实现代码却有所差异,在2.7.4.1版本中,如下代码所示:


上图圈起来的request方法最终会走到这个地方,可以看到确实是返回了一个Future实例:


而newFuture方法源码如下,请记住这个方法,后面会进行对比:


同时通过源码可以看到在获取到Future实例后,紧接着调用了subscribeTo方法,实现方法如下:


用了Java 8的CompletableFuture,实现异步编程。

但是在2.7.5版本中,如下代码所示:


在request方法中多了个executor参数,而该参数就是的实现类就是ThreadlessExecutor。

接下来,和之前的版本一样,会通过newFuture方法去获取一个DefaultFuture对象:


通过和2.7.4.1版本的newFuture方法对比你会发现这个地方就大不一样了。虽然都是获取Future,但是Future里面的内容不一样了。

直接上个代码对比图,一目了然:


第二步:业务线程紧接着调用 future.get 阻塞等待业务结果返回。

由于Dubbo默认是同步调用,而同步和异步调用的区别我在第一篇文章《Dubbo 2.7新特性之异步化改造》中就进行了详细解析:


我们找到异步转同步的地方,先看2.7.4.1版本的如下代码所示:


而这里的asyncResult.get()对应的源码是,CompletableFuture.get():


而在2.7.5版本中对应的地方发生了变化:


变化就在这个asyncResult.get方法上。

在2.7.5版本中,该方法的实现源码是:


先说标号为②的地方,和2.7.4.1版本是一样的,都是调用的CompletableFuture.get()。但是多了标号为①的代码逻辑。而这段代码就是之前新的线程模型里面体现的地方,下面红框框起来的部分:


在调用 future.get() 之前(即调用标号为②的代码之前),先调用 ThreadlessExecutor.wait()(即标号为①处的逻辑),wait 会使业务线程在一个阻塞队列上等待,直到队列中被加入元素。

接下来再对比两个地方:

第一个地方:之前提到的WrappedChannelHandler,可以看到2.7.5版本其构造函数的改造非常大:


第二个地方:之前提到的Dispatcher,是需要再写一篇文章才能说的清楚的,我这仅仅是做一个抛砖引玉,提一下:


AllChannelHandler是默认的策略,证明代码如下:


首先还是看标号为②的地方,看起来变化很大,其实就是对代码进行了一个抽离,封装。sendFeedback方法如下,和2.7.4.1版本中标号为②的地方的代码是一样的:


所以我们重点对比一下两个标号为①的地方,它们获取executor的方法变了:

1
2
复制代码2.7.4.1版本的方法是getExecutorService()
2.7.5版本的方法是getPreferredExecutorService()

代码如下,大家品一品两个版本之前的差异:


主要翻译一下getPreferredExecutorService方法上的注释:

1
2
3
复制代码Currently, this method is mainly customized to facilitate the thread model on consumer side.
1. Use ThreadlessExecutor, aka., delegate callback directly to the thread initiating the call.
2. Use shared executor to execute the callback.

目前,使用这种方法主要是为了客户端的线程模型而定制的。

1.使用ThreadlessExceutor,aka.,将回调直接委托给发起调用的线程。
2.使用shared executor执行回调。

小声说一句:这里这个aka怎么翻译,我实在是不知道了。难道是嘻哈里面的AKA?大家好,我是宝石GEM,aka(又名) 你的老舅。又画彩虹又画龙的。


好了,导读就到这里了。能看到这个地方的人我相信已经不多了。还是之前那句话由于涉及到的变化代码非常的多,我这里仅仅起到一个导读的作用,如果读者想要详细了解相关变化,还需要自己仔细阅读源码。希望你能自己搭个Demo跑一跑,对比一下两个版本的差异。

Dubbo版本介绍

趁着这次的版本升级,也趁机介绍一下Dubbo目前的主要版本吧。

据刘军大佬的分享:Dubbo 社区目前主力维护的有 2.6.x 和 2.7.x 两大版本,其中:

2.6.x 主要以 bugfix 和少量 enhancements 为主,因此能完全保证稳定性。

2.7.x 作为社区的主要开发版本,得到持续更新并增加了大量新 feature 和优化,同时也带来了一些稳定性挑战。

为方便 Dubbo 用户升级,社区在以下表格对 Dubbo 的各个版本进行了总结,包括主要功能、稳定性和兼容性等,从多个方面评估每个版本,以期能帮助用户完成升级评估:



可以看到社区对于最新的2.7.5版本的升级建议是:不建议大规模生产使用。

同时你去看Dubbo最新的issue,有很多都是对于2.7.5版本的”吐槽”。

但是我倒是觉得2.7.5是Dubbo发展进程中浓墨重彩的一笔,该版本打响了对于 Dubbo向整个微服务云原生体系靠齐的第一枪。对于多语言的支持方向的探索。实现了对 HTTP/2 协议的支持,同时增加了与 Protobuf 的结合。

开源项目,共同维护。我们当然知道Dubbo不是一个完美的框架,但是我们也知道,它的背后有一群知道它不完美,但是仍然不言乏力、不言放弃的工程师,他们在努力改造它,让它趋于完美。我们作为使用者,我们少一点”吐槽”,多一点鼓励。只有这样我们才能骄傲的说,我们为开源世界贡献了一点点的力量,我们相信它的明天会更好。

向开源致敬,向开源工程师致敬。

总之,牛逼。

最后说一句

才疏学浅,难免会有纰漏,如果你发现了错误的地方,还请你留言给我指出来,我对其加以修改。

感谢您的阅读,我坚持原创,十分欢迎并感谢您的关注。

以上。

欢迎关注公众号【why技术】。在这里我会分享一些技术相关的东西,主攻java方向,用匠心敲代码,对每一行代码负责。偶尔也会荒腔走板的聊一聊生活,写一写书评,影评。愿你我共同进步。

公众号-why技术

公众号-why技术

本文转载自: 掘金

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

用它5分钟以后,我放弃用了四年的 Flask

发表于 2020-01-19

有一个非常简单的需求:编写一个 HTTP接口,使用 POST 方式发送一个 JSON 字符串,接口里面读取发送上来的参数,对其中某个参数进行处理,并返回。

如果我们使用 Flask 来开发这个接口,那么代码是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
复制代码from flask import Flask, request

app = Flask(__name__)


@app.route('/insert', methods=['POST'])
def insert():
info = request.json
name = info['name']
age = info['age']
age_after_10_years = age + 10
msg = f'此人名叫:{name},10年后,此人年龄:{age_after_10_years}'
return {'success': True, 'msg': msg}

代码看起来已经很简洁了。我们用requests发个请求看看效果,如下图所示:

看起来没什么问题。

现在,我搞点破坏,把age字段改成字符串,再运行一下:

不出所料,报错了。

现在我们把age字段改回数字,但是直接移除name字段:

又报错了。

为了防止用户不按规矩提交数据,我们必需在接口里面做好各种异常数据的判断。于是增加判断以后的代码变得复杂了:

1
2
3
4
5
6
7
8
9
10
11
12
复制代码@app.route('/insert', methods=['POST'])
def insert():
info = request.json
name = info.get('name', '')
if not name:
return {'success': False, 'msg': 'name 参数不可省略,不可为空!'}
age = info.get('age', 0)
if not isinstance(age, int):
return {'success': False, 'msg': 'age参数不是数字!'}
age_after_10_years = age + 10
msg = f'此人名叫:{name},10年后,此人年龄:{age_after_10_years}'
return {'success': True, 'msg': msg}

看来,用 Flask,虽然能让你用很短的代码写出一个能工作的项目。但要写成一个可以正常使用的项目,还是需要你自己写更多代码。

下面我们来看一下,现代化的 web 框架:FaskApi能把这个工程简化到什么程度:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
复制代码from fastapi import FastAPI
from pydantic import BaseModel

app = FastAPI()


class People(BaseModel):
name: str
age: int
address: str
salary: float

@app.post('/insert')
def insert(people: People):
age_after_10_years = people.age + 10
msg = f'此人名字叫做:{people.name},十年后此人年龄:{age_after_10_years}'
return {'success': True, 'msg': msg}

我们还是使用 requests 发一条信息给 FastApi 开发的 HTTP接口。对于正常数据,正常使用:

现在我们把age字段改成字符串:

返回友好的提示信息,告诉我类型错误:age 字段不是 integer。

我们再试一试把name字段去掉:

返回友好信息,提示值错误:name字段丢失。

整个过程中,对类型的检查全都由 FastApi 自己完成。我们省下来很多时间。

我用了 Flask 四年,但在使用了5分钟 FastApi 以后,我决定以后不再使用 Flask 了。

回过头来,我们好好介绍一下 FastApi。

使用pip或者pipenv即可安装 FastApi:

1
2
复制代码pip install fastapi
pipenv install fastapi

安装完成以后,我们来完成第一个 API:

1
2
3
4
5
6
7
8
复制代码from fastapi import FastAPI

app = FastAPI()


@app.get('/')
def index():
return {'message': '你已经正确创建 FastApi 服务!'}

这里的写法跟 Flask 几乎一致。只不过在 Flask 中,我们定义路由的装饰器为@app.route('/')。而这里写为@app.get('/')

如下图所示:

写好代码以后,我们需要使用uvicorn来运行 FastApi。首先使用pip或者pipenv安装uvicorn:

1
2
复制代码pip install uvicorn
pipenv install uvicorn

然后执行命令:

1
复制代码uvicorn main:app --reload

其中main表示我们的代码文件为main.py,app表示我们初始化的FastApi 对象的名字。--reload参数表示在修改了代码以后立即生效,不需要重启。

运行命令以后,我们访问http://127.0.0.1:8000可以看到接口已经正确返回了 JSON 格式的数据:

那么如何定义一个带参数的 GET 方法呢?我们再写一段代码:

1
2
3
4
复制代码@app.get('/query/{uid}')
def query(uid):
msg = f'你查询的 uid 为:{uid}'
return {'success': True, 'msg': msg}

写好代码以后,我们直接在浏览器里面访问新的地址,可以看到修改已经生效了,如下图所示:

如果想限定uid 只能是数字,不能是字符串怎么办呢?你只需要多加4个字符:

1
2
3
4
复制代码@app.get('/query/{uid}')
def query(uid: int):
msg = f'你查询的 uid 为:{uid}'
return {'success': True, 'msg': msg}

对函数query的参数使用类型标注,标注为 int 类型。现在我们再来访问一下接口:

当 query 后面的参数不是整数时,正常报错了。

我们再来看一下本文一开始的 POST 方法。在使用 Flask 的时候,我们需要手动验证用户 POST 提交上来的数据是什么格式的,字段对不对。

但使用 FastApi 的时候,我们只需要类型标注就能解决所有问题。首先我们导入from pydantic import BaseModel,然后继承BaseModel实现我们允许 POST 方法提交上来的数据字段和格式:

1
2
3
4
5
6
7
8
9
10
复制代码from pydantic import BaseModel

app = FastAPI()


class People(BaseModel):
name: str
age: int
address: str
salary: float

People这个类通过类型标注,指定了它里面的4个字段和他们的类型。现在,我们来实现 POST 方法:

1
2
3
4
5
复制代码@app.post('/insert')
def insert(people: People):
age_after_10_years = people.age + 10
msg = f'此人名字叫做:{people.name},十年后此人年龄:{age_after_10_years}'
return {'success': True, 'msg': msg}

insert函数的参数people通过类型标注指定为People类型。

当我们使用 POST 方式提交数据时,FastApi 自动会以People中定义的字段为基准来校验数据,发现不对就返回报错信息。

除了开发接口变得非常简单外,FastApi 还会自动帮我们生成接口文档。大家访问http://127.0.0.1:8000/docs,可以看到接口文档已经自动生成好了:

这个接口不仅能看,而且直接就能在接口页面修改样例数据,发送请求,现场测试:

以上是对 FastApi 的极简介绍。有兴趣的同学可以查阅它的官方文档

最后,告诉大家,FastApi 是一个异步 Web 框架,它的速度非常非常非常快。远远超过 Flask。

FastApi 是最快的几个 Web 框架之一。速度可以匹敌 Golang写的接口。详细的对比可以看:one of the fastest Python frameworks available

本文转载自: 掘金

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

在并发编程中我们常说的“竞态”是什么? 1 何谓“竞态” 2

发表于 2020-01-19

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
37
38
39
40
41
42
43
复制代码public class MineRaceConditionDemo {
private int sharedValue = 0;
private final static int MAX = 1000;
private int raceCondition() {
if (sharedValue < MAX) {
sharedValue++;
} else {
sharedValue = 0;
}
return sharedValue;
}

public static void main(String[] args) {
MineRaceConditionDemo m = new MineRaceConditionDemo();
ExecutorService es = new ThreadPoolExecutor(10,
10,
5,
TimeUnit.MINUTES,
new ArrayBlockingQueue<Runnable>(1000),
new ThreadPoolExecutor.CallerRunsPolicy());
ConcurrentHashMap<Integer, Integer> map = new ConcurrentHashMap<>();

for (int i = 0; i < 1000; i++) {
es.execute(() -> {
try {
// 这是精髓所在啊,如果没有这个,那么要跑好几次才会出现竞态条件。
// 这个用来模拟程序中别的代码的处理时间。
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
int num = m.raceCondition();
if (map.get(num) != null) {
System.out.println("the repeat num: " + num);
System.out.println("happen.");
} else {
map.put(num, 0);
}
});
}
es.shutdown();
}
}

以上的代码是我自己的设计的一段会出现竞态条件的代码,比较简陋,但是可以说明问题了,你只要运行上面这段代码,每次的输出的结果级大概率都是不同的,但是也有以外,比如你的电脑的性能很强,这段代码也会出现执行正确的情况,也就是啥也不输出。
比如有的时候输出这个:

1
2
3
4
5
6
7
8
9
10
复制代码the repeat num: 78
happen.
the repeat num: 229
happen.
the repeat num: 267
happen.
the repeat num: 267
happen.
the repeat num: 498
happen.

有点时候输出这个:

1
2
3
4
复制代码the repeat num: 25
happen.
the repeat num: 157
happen.

当然,以上的是我的输出的,你们的输出肯定也是不同的。
对于上面这些,同一段代码,对于同样的输出,但是程序的输出有的时候是正确,有的时候是错误的,这种情况,我们称之为“竞态”。最要命的就是,代码每次输出不是每次都错误,而是你不知道他什么时候会正确,什么时候会错误。
当然,如果以上的代码执行的情况就是,啥都不输出,所有的值都是唯一的。

2 “竞态”为什么会发生?

“竞态”的发生主要是因为多个线程都对一个共享变量(比如上面的 sharedValue 就属于共享变量)有读取-修改的操作。在某个线程读取共享变量之后,进行相关操作的时候,别的线程把这个变量给改了,从而导致结果出现了错误。

什么样的代码模式会发生“竞态”

这部分知识主要是来自《Java多线程编程实战指南 核心篇》。
这里书中提到,会发生竞态条件就是两个模式:read-modify-write(读-改-写)和 check-than-act(检测而后行动)。
当然,这里面的都有一个相同的操作过程:某个有读取这个“共享变量”的操作,然后别的线程有个修改这个变量的操作。这里有个重点,在多个线程中,起码有一个线程有更新操作;如果所有的线程都是读操作,那么就不存在什么竞态条件。
总体来说,就是要thread1#load - thread2#update。 这种的模式,起码是是要有两个线程的,而且其中某个线程肯定是要有更新“共享变量”操作的,另一个线程不管是读取变量还是更新变量都会出现错误(要么读取脏数据、要么丢失更新结果)。

3 如何消除“竞态”?

单以上面的操作来说,一般来说有两种解法方式,

3.1 加锁

加上synchronized关键字,保证每次只能有一个线程获取共享变量的使用权。当然这里也可以用 java.util.concurrent.locks.ReentrantLock 这样的显式锁进行加锁操作,思想都是一样。

1
2
3
4
5
6
7
8
复制代码private synchronized int raceCondition() {
if (sharedValue < MAX) {
sharedValue++;
} else {
sharedValue = 0;
}
return sharedValue;
}

3.2 利用原子操作

利用java的工具包里的 AtomicInteger,代替int,利用原子操作消除“竞态”。

1
2
3
4
5
6
7
8
9
10
复制代码private AtomicInteger sharedValue = new AtomicInteger(0);
private final static int MAX = 1000;
private int raceCondition() {
if (sharedValue.get() < MAX) {
return sharedValue.getAndIncrement();
} else {
sharedValue.set(0);
return sharedValue.get();
}
}

以上两种方法的要义就是保证每个线程在操作“共享变量”的都是原子操作。

本文转载自: 掘金

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

Falco 进入 CNCF Incubator 项目 云

发表于 2020-01-19

作者 | 王思宇、陈洁、敖小剑

业界要闻

  1. Falco 进入 CNCF Incubator 项目

原于 2018 年 8 月进入 sandbox,旨在 Kubernetes 运行时环境下支持配置规则来加强应用安全性、降低风险。

  1. Kubernetes v1.17.1 发布

解决部分 cloud provider 和 kubelet 相关问题,比如:

  • kubelet 更新 Pod ready status 失败
  • kubelet 清理 Pod volumes 发生 panic
  1. CFP 2020 K8s Contributor Summit Amsterdam 开始征集 talk

将与 KubeCon 2020 EU 同期进行,欢迎 K8s contributor 参会。

  1. Istio 1.14.3 发布

Istio 发布 1.4.3 版本,此版本修复看一些 bug 以提高系统鲁棒性和用户体验。

上游重要进展

Kubenetes

  1. LoadBalancer Service 支持多种不同 protocol 类型的 port

目前一个 LoadBalancer Service 可以写多个 port,但是这些 port 的类型必须相同,比如都是 tcp 或 udp。这个 PR 允许在一个 LoadBalancer Service 中定义多种不同类型的 port,以支持不同云厂商提供的 service 服务。(对应的 PR)

  1. 解决组件特定 ComponentConfig 的配置问题

ComponentConfig 是支持编写 Kubernetes-style 的配置文件,来给各种 Kubernetes 核心组件作为启动配置,而不是直接通过命令行参数的方式配置,这个 KEP 是为了解决在编写 ComponentConfig 的时候不同组件的特定配置问题。

  1. device manager 中增加 release api 接口

在 Kubelet devicemanager 中增加 release 接口,支持 device plugin 释放已经分配给 Pod 的设备。

1
2
3
4
5
6
复制代码type Manager interface {
// ...

// Release release devices allocated to pods.
Release(pod *v1.Pod) error
}
  1. PDB status 中新增 conditions

conditions 用于上报当前 PDB 的一些状态信息,比如 PodDisruptionBudgetFailure(Failure),用于 disruption controller 在 failSafe 阶段上报状态标识。

1
2
3
4
5
6
7
8
复制代码type PodDisruptionBudgetStatus struct {
// ...

// Conditions represents the latest available observations of a PDB's current state.
// +patchMergeKey=type
// +patchStrategy=merge
Conditions []PodDisruptionBudgetCondition
}

Istio

  1. 为Telemetry V2 开启 TCP 元数据交换

Telemetry V2 依靠对等代理之间的元数据交换,以便它们可以在不依赖于 side lookup 的情况下产生丰富的遥测信息。 Istio 1.4 使用 “x-envoy-peer-metadata” http header 来支持 http 流量的元数据交换。Istio 1.5 将支持 TCP 流量的元数据交换,该提案目前已经得到批准。

  1. 在 AuthorizationPolicy 实现 deny 和 exclude

Istioi 社区提出更改 AuthorizationPolicy 的 API,以支持拒绝 (deny) 和排除 (exclude) 语义。目标包括支持通过使用 AuthorizationPolicy 来拒绝请求,并支持在 AuthorizationPolicy 中使用否定匹配 (not_XXX)。用户无需复制或修改其现有策略即可使用新功能。

  1. 可验证的自定义属性

在 SPIFFE 标识(service account 和 namespace)之外,允许客户在 Istio 授权中创建和使用可验证的自定义属性。当前这个提案还处于早期阶段,讨论动机和用例,收集反馈,尚未开始设计。

开源项目推荐

  1. Flux

一个面向 GitOps 流程的 operator(CNCF sandbox 项目),支持监听 Git 变化并自动触发一系列打包部署等操作。

  1. Kubeless

符合原生 Kubernetes 模式的 serverless framework。安装部署之后,只需要提交自己写的 code 以及依赖给 kubeless cli,由 kubeless 负责部署运行。

本周阅读推荐

  1. 《Manage Thousands of Clusters with GitOps and the Cluster API》

Weaveworks 团队如何通过 GitOps 和 Cluster API 来管理数千个 Kubernetes 集群。其中 GitOps 正是使用了上面开源项目推荐中介绍的 Flux 工具,来把 GitOps 链路打通,并结合 Cluster API 组成了 GitOps 模式的多集群管理。

  1. 《Vault replication across multiple datacenters on Kubernetes》

本文介绍了基于 Kubernetes 之上,如何管理跨多个数据中心的 Vault 集群。

  1. 《Kubernetes Networking Demystified - A Brief Guide》

本文从一次网络连接开始,介绍了 Kubernetes 中各类网络链路和配置,包括 Service、Load balancer、kube-proxy、Pod 网络等,推荐对 Kubernetes 网络机制感兴趣的同学阅读。

  1. 《从零开始入门 K8s | GPU 管理和 Device Plugin 工作机制》

本文主要介绍 K8s 中 GPU 管理方式、如何为容器配置 GPU,以及对应的 Extended Resource 和 Device Plugin的工作原理。

  1. 《K8s 实践 | 如何解决多租户集群的安全隔离问题?》

如何解决多租户集群的安全隔离问题是企业上云的一个关键问题,本文主要介绍了 Kubernetes 多租户集群的基本概念和常见应用形态,以及在企业内部共享集群的业务场景下,基于 Kubernetes 原生和 ACK 集群现有安全管理能力快速实现多租户集群的相关方案。

云原生实践峰会即将开幕

大咖海报.jpeg

“阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术领域、聚焦云原生流行技术趋势、云原生大规模的落地实践,做最懂云原生开发者的公众号。”

本文转载自: 掘金

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

什么?项目里gradle代码超过200行了!你可能需要 Ko

发表于 2020-01-19

本文内容来自博文 I hated Gradle! Kotlin and the buildSrc Plugin made me love it
英文好的可直接前往

注意科学上网

gradle是如何帮助我们构建的?对于这个问题大所数人可能是这样的

图片摘自上述博文 I hated Gradle! Kotlin and the buildSrc Plugin made me love it

.

造成问题的真相

首先要明确的事情:Android-Gradle-Plugin != Gradle

造成上述问题的原因是 Groovy

大多数 Android 开发者并没有在“真正的项目”中使用 groovy ,所以对于我们来说 Android 中的 build.gradle 文件就像是魔术师手中的魔法。

.

救世主:Kotlin & the buildSrc module?

2019年1月23日,Kotlin 1.3.20 发布,并在多平台项目中提供了对Kotlin DSL构建脚本的支持。

构建脚本中可以完成自动补全

我们可以像之前一样写代码,阅读文档,点击进去看到里面的实现,之前的烦恼不再有了

使用 buildSrc 文件夹

关于 builSrc 的职能和使用这里不再赘述,网上文章很多

Gradle dependency management with Kotlin (buildSrc)

Kotlin + buildSrc for Better Gradle Dependency Management

随着时间的推移,我们的 build.gradle 中的代码越来越长

配置 product flavor ,格式化apk文件命名以及路径,动态生成版本号,如果 jenkins 还需要配置一些独立的逻辑那么这个文件会越来越长

我们同一个项目的多个Android Module 大多是公共的样本代码,如果需要更改配置信息,则需要在所有的文件中修改

虽然我们可以使用apply form “…” 将部分逻辑抽取到 xx.gralde 中,可以使用 google 推荐的 ext 配置版本号,targetVersion 等信息。

但是这样真的足够灵活并方便拓展吗?

那么如何使用 kotlin + buildSrc 更改构建文件?

让我们看一下修改之前,这是一个非常庞大的文件,包含近200行代码


点击查看完整代码
.

使用上述方法仅用34行代码即可重构为一段易于理解的代码

.


点击查看完整代码

那么这段新的脚本文件都做了什么?

  1. 依赖的项目的特有插件 com.quickbirdstudios.bluesqaure
  2. 使用了项目自定义的扩展 extension
  3. 定义了module 特有的依赖

.

下面我们来看看究竟是如何实现的吧

步骤1 配置自定义Gralde 插件

  • 在项目根目录创建名为buildSrc文件夹
  • 为该文件夹创建build脚本,buildSrc/build.gradle.kts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
复制代码plugins {
`kotlin-dsl`
}

repositories {
mavenCentral()
google()
jcenter()
}

dependencies {
/* Example Dependency */
/* Depend on the android gradle plugin, since we want to access it in our plugin */
implementation("com.android.tools.build:gradle:3.5.3")

/* Example Dependency */
/* Depend on the kotlin plugin, since we want to access it in our plugin */
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.3.61")

/* Depend on the default Gradle API's since we want to build a custom plugin */
implementation(gradleApi())
implementation(localGroovy())
}

现在我们实现我们自己的插件,例如:MyPlugin

.

详情参见官方文档,如何创建Gralde Plugin

.

这里使用 kotlin 语言

.

首先创建 MyPlugin class文件 implements Plugin<Project>

1
2
3
4
5
6
7
复制代码import org.gradle.api.Plugin
import org.gradle.api.Project

open class MyPlugin : Plugin<Project> {
override fun apply(project: Project) {
}
}

注意导包不要导错

配置自定义插件的id,例如 com.plugin.test

创建这样的文件 buildSrc/src/main/resources/META-INF/gradle-plugins/com.plugin.test.properties

com.plugin.test.properties 该文件名对应着插件id

在该文件中加入MyPlugin的全路径(包括包名)

1
复制代码implementation-class=com.test.MyPlugin

现在我们可以在所有的module中使用该插件了

1
2
3
4
5
复制代码plugins {
// ...
id("com.android.library")
id("com.plugin.test")
}

这样的处理会使我们插件内部的 apply 函数(方法)得到调用

步骤2 将公共代码抽取到自定义插件中

在我们配置Android 配置文件时我们会有这样的代码

1
2
3
4
复制代码android {
compileSdkVersion(29)
// ...
}

android{}代码块被称为 扩展(Extension)

这个函数的receiver(不知道如何翻译更合适) 实现了 AppExtension

这里贴一下源码

1
2
3
4
5
6
7
8
9
10
11
复制代码/**
* Retrieves the [android][com.android.build.gradle.AppExtension] extension.
*/
val org.gradle.api.Project.`android`: com.android.build.gradle.AppExtension get() =
(this as org.gradle.api.plugins.ExtensionAware).extensions.getByName("android") as com.android.build.gradle.AppExtension

/**
* Configures the [android][com.android.build.gradle.AppExtension] extension.
*/
fun org.gradle.api.Project.`android`(configure: com.android.build.gradle.AppExtension.() -> Unit): Unit =
(this as org.gradle.api.plugins.ExtensionAware).extensions.configure("android", configure)

.

我们没必要在所有的module的build.gradle.kts中都配置一次,可以将这段逻辑抽取到一个当我们的插件被应用时的函数中:

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
复制代码// MyPlugin.kt
open class BluesquarePlugin : Plugin<Project> {
override fun apply(project: Project) {
project.configureAndroid()
}
}

// Android.kt
internal fun Project.configureAndroid() = this.extensions.getByType<AppExtension>().run {
compileSdkVersion(29)
defaultConfig {
minSdkVersion(21)
targetSdkVersion(28)
versionCode = 2
versionName = "1.0.1"
testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner"
}

buildTypes {
getByName("release") {
isMinifyEnabled = false
proguardFiles(getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro")
}

getByName("debug") {
isTestCoverageEnabled = true
}
}

packagingOptions {
exclude("META-INF/NOTICE.txt")
// ...
}

compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
}

.

之后我们创建一个新的Android module 就很简单了,我们只需应用我们的插件com.test.MyPlugin即可自动配置android{}代码块

1
2
3
4
5
6
7
8
9
10
11
复制代码plugins {
id("com.android.library")
id("com.test.MyPlugin")
}

dependencies {
implementation(kotlin("stdlib-jdk8"))
testImplementation("junit:junit:4.12")
andriodTestImplementation("com.android.support.test:runner:1.0.2")
androidTestImplementation("com.android.support.test.espresso:espresso-core:3.0.2")
}

注意:只需使用android{}代码块再次配置即可覆盖插件中预置的配置

·

步骤3 配置默认依赖(dependencies)

一般来说,我们每个android 都需使用下列依赖

  • Kotlin Standard library
  • JUnit
  • Support Test Runner
  • Espresso

我们可以在插件中添加另一个函数 configureDependencies

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
复制代码// MyPlugin.kt
open class MyPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.configureAndroid()
project.configureDependencies()
}
}

// Dependencies.kt
const val jUnit = "junit:junit:4.12"
const val androidTestRunner = "com.android.support.test:runner:1.0.2"
const val androidTestRules = "com.android.support.test:rules:1.0.2"
const val mockkAndroid = "io.mockk:mockk-android:1.9"
const val mockk = "io.mockk:mockk:1.9"
const val espressoCore = "com.android.support.test.espresso:espresso-core:3.0.2"

internal fun Project.configureDependencies() = dependencies {
add("testImplementation", jUnit)

if (project.containsAndroidPlugin()) {
add("androidTestImplementation", androidTestRunner)
add("androidTestImplementation", androidTestRules)
add("androidTestImplementation", espressoCore)
}
}

internal fun Project.containsAndroidPlugin(): Boolean {
return project.plugins.toList().any { plugin -> plugin is AndroidBasePlugin }
}

.

现在创建Android module只需

1
2
3
4
复制代码plugins {
id("com.android.library")
id("com.test.MyPlugin")
}

.

步骤4 配置默认插件

上文的配置有一个缺陷:在Android plugin加载之后我们配置的插件才会正常工作。

但如果我们的所有module都是Android module,我们可以用自定义插件去管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
复制代码// MyPlugin.kt
open class BluesquarePlugin : Plugin<Project> {
override fun apply(project: Project) {
project.configurePlugins()
project.configureAndroid()
project.configureDependencies()
}
}

//Plugins.kt
internal fun Project.configurePlugins() {
plugins.apply("com.android.library")
plugins.apply("org.gradle.maven-publish")
//其他公共插件
}

.

现在创建新的Android module只需

1
2
3
复制代码plugins {
id("com.test.MyPlugin")
}

上述配置可以为我们应用 Android Library Plugin 并配置了android代码块和默认依赖

.

如果想创建不加载Android plguin的纯 java module怎么办?

很简单,我们可以多创建几个插件(不要命名为buildSrc)

例如我可以创建 MyBasePlugin, MyAndroidPlugin, MyJavaPlugin, MyMultiplatformPlugin

.

让你的插件变为可配置的

.

我们可以在应用了 Android Library 或者 Android Application 插件的module中使用android{}代码块配置

1
2
3
复制代码android {
compileSdkVersion(29)
}

.

Gradle API 将此定义为 Extension

我们也可以定义一些自定义的扩展

.

例如我们想处理两个配置

  1. 这个module发布了吗?
  2. 被发布时packageName是什么?

.

配置好后我们可以这样使用

1
2
3
4
5
6
7
8
复制代码plugins {
id("com.plugin.test")
}

test {
publish = true
packageName = "my-package"
}

.

我们只需创建一个类

1
2
3
4
复制代码open class TestExtension {
var publish: Boolean = false
var packageName: String = ""
}

.

接下来告诉Gradle有哪些配置

1
2
3
4
5
6
7
8
9
10
11
12
复制代码// MyPlugin.kt
open class BluesquarePlugin : Plugin<Project> {
override fun apply(project: Project) {
val testExtension: TestExtension = project.extensions.create(
"test", TestExtension::class.java
)

project.configurePlugins()
project.configureAndroid()
project.configureDependencies()
}
}

.

原文作者总结了他们公司使用 buildSrc plugin 只用7行代码便实现了以下工作

  • Configure our Android builds
  • Apply default plugins
  • Apply default dependencies
  • Run a custom linter
  • Run aggregated coverage reports
  • Run aggregated test result reports
  • Install default git hooks into the project
  • Setup Multiplatform builds
  • Setup publications for our library modules
  • Deploy libraries to the correct repository (Snapshot/Release)

.

以上为原作者表达的主要信息,原文请移步

.

demo代码

.

英文水平有限,如有纰漏请指教。

本文转载自: 掘金

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

如何设计一个优雅的RESTFUL的接口

发表于 2020-01-19

show me the code and talk to me,做的出来更要说的明白

我是布尔bl,你的支持是我分享的动力!

一 、引入

设计接口是我们开发人员的日常操作。当我们把接口交给前端人员时,是否有种拔剑出鞘的错觉。毕竟交付接口,我们的开发工作就阶段性完成了。不过,如果我们没有一个接口设计规范的时候,结果会怎样呢?我们来张图感受一下。

二、REST

2000年,一个年轻小伙子(Roy Thomas Fielding)在他的博士论文提出了 REST。REST 是一种万维网软件架构风格。为什么说是风格不是标准呢?个人理解可能说标准就有点过分了。小伙子做不到。随后这种风格被推广开来,漂洋过海,被大众熟知。在 REST 的基础上,产生了 RESTFUL 。什么是 RESTFUL?简单的说,符合 REST 风格的接口就是 RESTFUL。

三、RESTFUL

接口各种各样。正如一千个人眼里有一千个鲁迅。RESTFUL 接口究竟长什么样子呢?

3.1 HTTP 的方法

HTTP 里面有几个基本的方法。我们利用这些约定一些规范。

方法 作用
GET 获取数据
POST 插入数据
PUT 更新数据
DELETE 删除数据

从表中,如果我们可以清楚看到当我们的接口是关于获取数据,那么我们使用 GET 方法。

如果我们可以清楚看到当我们的接口是关于插入数据,那么我们使用 POST 方法。

如果我们可以清楚看到当我们的接口是关于更新数据,那么我们使用 PUT 方法。

如果我们可以清楚看到当我们的接口是关于删除数据,那么我们使用 DELECT 方法。

3.2 名词

在上面我们已经知道接口在什么时候需要什么方法,那么我们现在来进入到设计接口的第二步。

我们看看线上网站的接口是怎么样的。

图中我们可以看到有个 v1 ,他代表的是版本号,所以我们在设计接口的时候可以在将版本号写上,用 v1、v2、v3 等表示。

我们发现他的接口都是名词。所以我们知道 RESTFUL 接口使用的是名词。比如我们设计一个获取数据的接口,我们可这样设计

1
复制代码/v1/list

上面接口是获取所有数据。

当我们需要列表中的一条数据,我们可以这样设计

1
复制代码/v1/list/1

上面接口表示获取是列表的1号数据,我们可以获取2号、3号数据等等,只要改变数字即可。

3.3 组合

结合上面两步,我们就可以设计出增删改查的 restful 接口了。

接口 方法 作用
/v1/list GET 获取列表
/v1/list POST 增加列表
/v1/list PUT 更新列表
/v1/list DELETE 删除列表

3.4 应用

以下是源码的demo

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
复制代码import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

@SpringBootApplication
@RestController
@Slf4j
public class LsbRestfulApplication {

public static void main(String[] args) {
SpringApplication.run(LsbRestfulApplication.class, args);
}

/**
* 得到所有列表
* @return
*/
@RequestMapping(value = "/v1/list",method = RequestMethod.GET)
public String getList(){
log.info("得到列表");
return "得到列表";
}

/**
* 得到列表中的一条
* @param name
* @return
*/
@RequestMapping(value = "/v1/list/{name}",method = RequestMethod.GET)
public String getListone(@PathVariable("name") String name){
log.info("得到列表"+name);
return "得到列表"+name;
}

/**
* 往列表中的增加一条数据
* @return
*/
@RequestMapping(value = "/v1/list",method = RequestMethod.POST)
public String addList(){
log.info("增加一个列表1");
return "增加一个列表1";
}

/**
* 更新列表中的一条数据
* @return
*/
@RequestMapping(value = "/v1/list/{name}",method = RequestMethod.PUT)
public String updateListOne(@PathVariable("name") String name){
log.info("更新列表"+name);
return "更新列表"+name;
}

/**
* 删除所有列表
* @return
*/
@RequestMapping(value = "/v1/list",method = RequestMethod.DELETE)
public String delList(){
log.info("删除一个列表");
return "删除一个列表";
}
}

四、某同城交友网站

github.com/buerbl

暗号:荆轲刺秦王

公众号

本文转载自: 掘金

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

1…835836837…956

开发者博客

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