CS-Notes/notes/分布式问题分析.md

293 lines
15 KiB
Markdown
Raw Normal View History

2018-06-04 14:28:30 +08:00
# 一、分布式事务
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
指事务的操作位于不同的节点上需要保证事务的 AICD 特性。例如在下单场景下库存和订单如果不在同一个节点上就需要涉及分布式事务。
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
## 两阶段提交协议
2018-05-31 21:01:23 +08:00
2018-06-04 14:28:30 +08:00
Two-phase Commit2PC
2018-05-31 21:01:23 +08:00
两类节点协调者Coordinator和参与者Participants协调者只有一个参与者可以有多个。
2018-06-04 14:28:30 +08:00
### 1. 运行过程
2018-05-31 21:01:23 +08:00
2018-06-04 14:28:30 +08:00
① 准备阶段:协调者询问参与者事务是否执行成功;
2018-05-31 21:01:23 +08:00
2018-06-04 14:28:30 +08:00
![](index_files/c8dbff58-d981-48be-8c1c-caa6c2738791.jpg)
2018-05-31 21:01:23 +08:00
2018-06-04 14:28:30 +08:00
② 提交阶段:如果事务在每个参与者上都执行成功,协调者发送通知让参与者提交事务;否则,协调者发送通知让参与者回滚事务。
2018-05-31 21:01:23 +08:00
2018-06-04 14:28:30 +08:00
![](index_files/aa844ff0-cd16-4478-b415-da071b615a17.jpg)
2018-05-31 21:01:23 +08:00
需要注意的是,在准备阶段,参与者执行了事务,但是还未提交。只有在提交阶段接收到协调者发来的通知后,才进行提交或者回滚。
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
### 2. 分析
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
2PC 可以保证强一致性但是因为在准备阶段协调者需要等待所有参与者的结果才能进入提交阶段因此可用性差。
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
### 3. 存在的问题
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
- 参与者发生故障。解决方案:可以给事务设置一个超时时间,如果某个参与者一直不响应,那么认为事务执行失败。
- 协调者发生故障。解决方案:将操作日志同步到备用协调者,让备用协调者接替后续工作。
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
### 4. XA 协议
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
XA 协议是多数数据库的 2PC 协议的实现包含了事务管理器和本地资源管理器。
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
## 本地消息
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
### 1. 原理
2018-05-31 21:01:23 +08:00
本地消息表与业务数据表处于同一个数据库中,这样就能利用本地事务来保证在对这两个表的操作满足事务特性。
2018-06-04 14:28:30 +08:00
1. 在分布式事务操作的一方,它完成写业务数据的操作之后向本地消息表发送一个消息,本地事务能保证这个消息一定会被写入本地消息表中。
2. 之后将本地消息表中的消息转发到 Kafka 等消息队列MQ如果转发成功则将消息从本地消息表中删除否则继续重新转发。
3. 在分布式事务操作的另一方从消息队列中读取一个消息,并执行消息中的操作。
2018-05-31 21:01:23 +08:00
2018-06-04 14:28:30 +08:00
![](index_files/e3bf5de4-ab1e-4a9b-896d-4b0ad7e9220a.jpg)
2018-05-31 21:01:23 +08:00
2018-06-04 14:28:30 +08:00
### 2. 分析
2018-05-31 21:01:23 +08:00
本地消息表利用了本地事务来实现分布式事务,并且使用了消息队列来保证最终一致性。
2018-06-04 14:28:30 +08:00
# 二、分布式锁
2018-05-31 21:01:23 +08:00
2018-06-04 14:28:30 +08:00
可以使用 Java 提供的内置锁来实现进程同步 JVM 实现的 synchronized  JDK 提供的 Lock。但是在分布式场景下需要同步的进程可能位于不同的节点上那么就需要使用分布式锁来同步。
2018-05-31 21:01:23 +08:00
2018-06-04 14:28:30 +08:00
## 原理
2018-05-31 21:01:23 +08:00
2018-06-04 14:28:30 +08:00
锁可以有阻塞锁和乐观锁两种实现方式这里主要探讨阻塞锁实现。阻塞锁通常使用互斥量来实现互斥量为 1 表示有其它进程在使用锁此时处于锁定状态互斥量为 0 表示未锁定状态。1  0 可以用一个整型值来存储也可以用某个数据存在或者不存在来存储某个数据存在表示互斥量为 1也就是锁定状态。
2018-05-31 21:01:23 +08:00
2018-06-04 14:28:30 +08:00
## 实现
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
### 1. 数据库的唯一索引
2018-04-06 22:46:59 +08:00
2018-05-31 21:01:23 +08:00
当想要获得锁时,就向表中插入一条记录,释放锁时就删除这条记录。唯一索引可以保证该记录只被插入一次,那么就可以用这个记录是否存在来判断是否存于锁定状态。
2018-04-06 22:46:59 +08:00
2018-05-31 21:01:23 +08:00
这种方式存在以下几个问题:
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
- 锁没有失效时间,解锁失败会导致死锁,其他线程无法再获得锁。
- 只能是非阻塞锁,插入失败直接就报错了,无法重试。
- 不可重入,同一线程在没有释放锁之前无法再获得锁。
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
### 2. Redis  SETNX 指令
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
使用 SETNXset if not exist指令插入一个键值对如果 Key 已经存在那么会返回 False否则插入成功并返回 True。
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
SETNX 指令和数据库的唯一索引类似可以保证只存在一个 Key 的键值对可以用一个 Key 的键值对是否存在来判断是否存于锁定状态。
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
EXPIRE 指令可以为一个键值对设置一个过期时间从而避免了死锁的发生。
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
### 3. Redis  RedLock 算法
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
使用了多个 Redis 实例来实现分布式锁这是为了保证在发生单点故障时仍然可用。
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
- 尝试从 N 个相互独立 Redis 实例获取锁如果一个实例不可用应该尽快尝试下一个。
- 计算获取锁消耗的时间只有当这个时间小于锁的过期时间并且从大多数N/2+1实例上获取了锁那么就认为锁获取成功了。
- 如果锁获取失败,会到每个实例上释放锁。
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
### 4. Zookeeper 的有序节点
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
Zookeeper 是一个为分布式应用提供一致性服务的软件例如配置管理、分布式协同以及命名的中心化等这些都是分布式系统中非常底层而且是必不可少的基本功能但是如果自己实现这些功能而且要达到高吞吐、低延迟同时还要保持一致性和可用性实际上非常困难。
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
**(一)抽象模型**
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
Zookeeper 提供了一种树形结构级的命名空间/app1/p_1 节点表示它的父节点为 /app1。
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
<img src="index_files/31d99967-1171-448e-8531-bccf5c14cffe.jpg" width="400"/>
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
**(二)节点类型**
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
- 永久节点:不会因为会话结束或者超时而消失;
- 临时节点:如果会话结束或者超时就会消失;
- 有序节点:会在节点名的后面加一个数字后缀,并且是有序的,例如生成的有序节点为 /lock/node-0000000000它的下一个有序节点则为 /lock/node-0000000001依次类推。
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
**(三)监听器**
2018-04-06 22:46:59 +08:00
2018-05-31 21:01:23 +08:00
为一个节点注册监听器,在节点状态发生改变时,会给客户端发送消息。
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
**(四)分布式锁实现**
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
- 创建一个锁目录 /lock
- 在 /lock 下创建临时的且有序的子节点第一个客户端对应的子节点为 /lock/lock-0000000000第二个为 /lock/lock-0000000001以此类推
-  客户端获取 /lock 下的子节点列表判断自己创建的子节点是否为当前子节点列表中序号最小的子节点如果是则认为获得锁否则监听自己的前一个子节点获得子节点的变更通知后重复此步骤直至获得锁
- 执行业务代码,完成后,删除对应的子节点。
2018-05-31 21:01:23 +08:00
2018-06-04 14:28:30 +08:00
**(五)会话超时**
2018-05-31 21:01:23 +08:00
2018-06-04 14:28:30 +08:00
如果一个已经获得锁的会话超时了因为创建的是临时节点所以该会话对应的临时节点会被删除其它会话就可以获得锁了。可以看到Zookeeper 分布式锁不会出现数据库的唯一索引实现分布式锁的死锁问题。
2018-05-31 21:01:23 +08:00
2018-06-04 14:28:30 +08:00
**(六)羊群效应**
2018-05-31 21:01:23 +08:00
一个节点未获得锁,需要监听自己的前一个子节点,这是因为如果监听所有的子节点,那么任意一个子节点状态改变,其它所有子节点都会收到通知(羊群效应),而我们只希望它的后一个子节点收到通知。
2018-06-04 14:28:30 +08:00
# 三、分布式 Session
2018-05-31 21:01:23 +08:00
2018-06-04 14:28:30 +08:00
在分布式场景下一个用户的 Session 如果只存储在一个服务器上那么当负载均衡器把用户的下一个请求转发到另一个服务器上该服务器没有用户的 Session就可能导致用户需要重新进行登录等操作。
2018-05-31 21:01:23 +08:00
2018-06-04 14:28:30 +08:00
![](index_files/cookiedata.png)
2018-05-31 21:01:23 +08:00
2018-06-04 14:28:30 +08:00
### 1. Sticky Sessions
2018-05-31 21:01:23 +08:00
2018-06-04 14:28:30 +08:00
需要配置负载均衡器使得一个用户的所有请求都路由到一个服务器节点上这样就可以把用户的 Session 存放在该服务器节点中。
2018-05-31 21:01:23 +08:00
2018-06-04 14:28:30 +08:00
缺点当服务器节点宕机时将丢失该服务器节点上的所有 Session。
2018-05-31 21:01:23 +08:00
2018-06-04 14:28:30 +08:00
![](index_files/MultiNode-StickySessions.jpg)
2018-05-31 21:01:23 +08:00
2018-06-04 14:28:30 +08:00
### 2. Session Replication
2018-05-31 21:01:23 +08:00
2018-06-04 14:28:30 +08:00
在服务器节点之间进行 Session 同步操作这样的话用户可以访问任何一个服务器节点。
2018-05-31 21:01:23 +08:00
缺点:需要更好的服务器硬件条件;需要对服务器进行配置。
2018-06-04 14:28:30 +08:00
![](index_files/MultiNode-SessionReplication.jpg)
2018-05-31 21:01:23 +08:00
2018-06-04 14:28:30 +08:00
### 3. Persistent DataStore
2018-05-31 21:01:23 +08:00
2018-06-04 14:28:30 +08:00
 Session 信息持久化到一个数据库中。
2018-05-31 21:01:23 +08:00
2018-06-04 14:28:30 +08:00
缺点有可能需要去实现存取 Session 的代码。
2018-05-31 21:01:23 +08:00
2018-06-04 14:28:30 +08:00
![](index_files/MultiNode-SpringSession.jpg)
2018-05-31 21:01:23 +08:00
2018-06-04 14:28:30 +08:00
### 4. In-Memory DataStore
2018-05-31 21:01:23 +08:00
2018-06-04 14:28:30 +08:00
可以使用 Redis  Memcached 这种内存型数据库对 Session 进行存储可以大大提高 Session 的读写效率。内存型数据库同样可以持久化数据到磁盘中来保证数据的安全性。
2018-05-31 21:01:23 +08:00
2018-06-04 14:28:30 +08:00
# 四、负载均衡
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
## 算法
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
### 1. 轮询Round Robin
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
轮询算法把每个请求轮流发送到每个服务器上。下图中一共有 6 个客户端产生了 6 个请求 6 个请求按 (1, 2, 3, 4, 5, 6) 的顺序发送。最后,(1, 3, 5) 的请求会被发送到服务器 1(2, 4, 6) 的请求会被发送到服务器 2。
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
![](index_files/2766d04f-7dad-42e4-99d1-60682c9d5c61.jpg)
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
该算法比较适合每个服务器的性能差不多的场景如果有性能存在差异的情况下那么性能较差的服务器可能无法承担过大的负载下图的 Server 2
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
![](index_files/f7ecbb8d-bb8b-4d45-a3b7-f49425d6d83d.jpg)
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
### 2. 加权轮询Weighted Round Robbin
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
加权轮询是在轮询的基础上根据服务器的性能差异为服务器赋予一定的权值。例如下图中服务器 1 被赋予的权值为 5服务器 2 被赋予的权值为 1那么 (1, 2, 3, 4, 5) 请求会被发送到服务器 1(6) 请求会被发送到服务器 2。
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
![](index_files/211c60d4-75ca-4acd-8a4f-171458ed58b4.jpg)
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
### 3. 最少连接least Connections
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
由于每个请求的连接时间不一样,使用轮询或者加权轮询算法的话,可能会让一台服务器当前连接数过大,而另一台服务器的连接过小,造成负载不均衡。例如下图中,(1, 3, 5) 请求会被发送到服务器 1但是 (1, 3) 很快就断开连接,此时只有 (5) 请求连接服务器 1(2, 4, 6) 请求被发送到服务器 2只有 (2) 的连接断开。该系统继续运行时服务器 2 会承担过大的负载。
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
![](index_files/3b0d1aa8-d0e0-46c2-8fd1-736bf08a11aa.jpg)
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
最少连接算法就是将请求发送给当前最少连接数的服务器上。例如下图中服务器 1 当前连接数最小那么新到来的请求 6 就会被发送到服务器 1 上。
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
![](index_files/1f4a7f10-52b2-4bd7-a67d-a9581d66dc62.jpg)
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
### 4. 加权最少连接Weighted Least Connection
2018-04-06 22:46:59 +08:00
2018-05-25 15:47:52 +08:00
在最少连接的基础上,根据服务器的性能为每台服务器分配权重,再根据权重计算出每台服务器能处理的连接数。
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
![](index_files/44edefb7-4b58-4519-b8ee-4aca01697b78.jpg)
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
### 5. 随机算法Random
2018-04-06 22:46:59 +08:00
把请求随机发送到服务器上。和轮询算法类似,该算法比较适合服务器性能差不多的场景。
2018-06-04 14:28:30 +08:00
![](index_files/0ee0f61b-c782-441e-bf34-665650198ae0.jpg)
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
### 6. 源地址哈希法 (IP Hash)
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
源地址哈希通过对客户端 IP 哈希计算得到的一个数值用该数值对服务器数量进行取模运算取模结果便是目标服务器的序号。
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
- 优点保证同一 IP 的客户端都会被 hash 到同一台服务器上。
- 缺点不利于集群扩展后台服务器数量变更都会影响 hash 结果。可以采用一致性 Hash 改进。
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
![](index_files/2018040302.jpg)
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
## 实现
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
### 1. HTTP 重定向
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
HTTP 重定向负载均衡服务器收到 HTTP 请求之后会返回服务器的地址并将该地址写入 HTTP 重定向响应中返回给浏览器浏览器收到后需要再次发送请求。
2018-04-06 22:46:59 +08:00
缺点:
2018-06-04 14:28:30 +08:00
- 用户访问的延迟会增加;
- 如果负载均衡器宕机,就无法访问该站点。
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
![](index_files/10bdf7bf-0daa-4a26-b927-f142b3f8e72b.png)
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
### 2. DNS 重定向
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
使用 DNS 作为负载均衡器根据负载情况返回不同服务器的 IP 地址。大型网站基本使用了这种方式做为第一级负载均衡手段然后在内部使用其它方式做第二级负载均衡。
2018-04-06 22:46:59 +08:00
缺点:
2018-06-04 14:28:30 +08:00
- DNS 查找表可能会被客户端缓存起来那么之后的所有请求都会被重定向到同一个服务器。
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
![](index_files/f8b16d1e-7363-4544-94d6-4939fdf849dc.png)
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
### 3. 修改 MAC 地址
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
使用 LVSLinux Virtual Server这种链路层负载均衡器根据负载情况修改请求的 MAC 地址。
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
![](index_files/f0e35b7a-2948-488a-a5a9-97d3f6b5e2d7.png)
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
### 4. 修改 IP 地址
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
在网络层修改请求的目的 IP 地址。
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
![](index_files/265a355d-aead-48aa-b455-f33b62fe729f.png)
2018-04-06 22:46:59 +08:00
2018-06-04 14:28:30 +08:00
### 5. 代理自动配置
2018-04-06 22:46:59 +08:00
正向代理与反向代理的区别:
2018-06-04 14:28:30 +08:00
- 正向代理:发生在客户端,是由用户主动发起的。比如翻墙,客户端通过主动访问代理服务器,让代理服务器获得需要的外网数据,然后转发回客户端。
- 反向代理:发生在服务器端,用户不知道代理的存在。
PAC 服务器是用来判断一个请求是否要经过代理。
![](index_files/52e1af6f-3a7a-4bee-aa8f-fcb5dacebe40.jpg)
# 参考资料
- [Comparing Load Balancing Algorithms](http://www.jscape.com/blog/load-balancing-algorithms)
- [负载均衡算法及手段](https://segmentfault.com/a/1190000004492447)
- [Redirection and Load Balancing](http://slideplayer.com/slide/6599069/#)
- [Session Management using Spring Session with JDBC DataStore](https://sivalabs.in/2018/02/session-management-using-spring-session-jdbc-datastore/)
- [Apache Wicket User Guide - Reference Documentation](https://ci.apache.org/projects/wicket/guide/6.x/)
- [集群/分布式环境下 5  Session 处理策略](http://blog.csdn.net/u010028869/article/details/50773174?ref=myread)
- [浅谈分布式锁](http://www.linkedkeeper.com/detail/blog.action?bid=1023)
- [深入理解分布式事务](https://juejin.im/entry/577c6f220a2b5800573492be)
- [分布式系统的事务处理](https://coolshell.cn/articles/10910.html)
- [关于分布式事务](http://blog.csdn.net/suifeng3051/article/details/52691210)
- [基于 Zookeeper 的分布式锁](http://www.dengshenyu.com/java/%E5%88%86%E5%B8%83%E5%BC%8F%E7%B3%BB%E7%BB%9F/2017/10/23/zookeeper-distributed-lock.html)
- [微服务场景下的数据一致性解决方案](https://opentalk.upyun.com/310.html)
- [聊聊分布式事务,再说说解决方案](https://www.cnblogs.com/savorboard/p/distributed-system-transaction-consistency.html)
---bottom---CyC---
![](index_files/c8dbff58-d981-48be-8c1c-caa6c2738791.jpg)
![](index_files/aa844ff0-cd16-4478-b415-da071b615a17.jpg)
![](index_files/e3bf5de4-ab1e-4a9b-896d-4b0ad7e9220a.jpg)
![](index_files/31d99967-1171-448e-8531-bccf5c14cffe.jpg)
![](index_files/cookiedata.png)
![](index_files/MultiNode-StickySessions.jpg)
![](index_files/MultiNode-SessionReplication.jpg)
![](index_files/MultiNode-SpringSession.jpg)
![](index_files/2766d04f-7dad-42e4-99d1-60682c9d5c61.jpg)
![](index_files/f7ecbb8d-bb8b-4d45-a3b7-f49425d6d83d.jpg)
![](index_files/211c60d4-75ca-4acd-8a4f-171458ed58b4.jpg)
![](index_files/3b0d1aa8-d0e0-46c2-8fd1-736bf08a11aa.jpg)
![](index_files/1f4a7f10-52b2-4bd7-a67d-a9581d66dc62.jpg)
![](index_files/44edefb7-4b58-4519-b8ee-4aca01697b78.jpg)
![](index_files/0ee0f61b-c782-441e-bf34-665650198ae0.jpg)
![](index_files/2018040302.jpg)
![](index_files/10bdf7bf-0daa-4a26-b927-f142b3f8e72b.png)
![](index_files/f8b16d1e-7363-4544-94d6-4939fdf849dc.png)
![](index_files/f0e35b7a-2948-488a-a5a9-97d3f6b5e2d7.png)
![](index_files/265a355d-aead-48aa-b455-f33b62fe729f.png)
![](index_files/52e1af6f-3a7a-4bee-aa8f-fcb5dacebe40.jpg)