mirror of
https://github.com/doocs/advanced-java.git
synced 2025-02-04 07:10:13 +08:00
docs: add hystrix-semphore-isolation, fix #12
- Add hystrix-semphore-isolation.md - Update RabbitMQ, fix #12 - Fix typo
This commit is contained in:
parent
de8dda7716
commit
4fe4d73eb0
@ -84,6 +84,7 @@
|
||||
- [电商网站详情页系统架构](/docs/high-availability/e-commerce-website-detail-page-architecture.md)
|
||||
- [Hystrix 线程池技术实现资源隔离](/docs/high-availability/hystrix-thread-pool-isolation.md)
|
||||
- [Hystrix 信号量机制实现资源隔离](/docs/high-availability/hystrix-semphore-isolation.md)
|
||||
- [Hystrix 隔离策略细粒度控制](/docs/high-availability/hystrix-execution-isolation.md)
|
||||
|
||||
### 高可用系统
|
||||
- 如何设计一个高可用系统?
|
||||
|
106
docs/high-availability/hystrix-execution-isolation.md
Normal file
106
docs/high-availability/hystrix-execution-isolation.md
Normal file
@ -0,0 +1,106 @@
|
||||
## Hystrix 隔离策略细粒度控制
|
||||
Hystrix 实现资源隔离,有两种策略:
|
||||
|
||||
- 线程池隔离
|
||||
- 信号量隔离
|
||||
|
||||
对资源隔离这一块东西,而对于资源隔离,其实可以做一定的细粒度的一些控制。
|
||||
|
||||
### execution.isolation.strategy
|
||||
指定了 HystrixCommand.run() 的资源隔离策略:`THREAD` or `SEMAPHORE`,一种基于线程池,一种基于信号量。
|
||||
|
||||
```java
|
||||
// to use thread isolation
|
||||
HystrixCommandProperties.Setter().withExecutionIsolationStrategy(ExecutionIsolationStrategy.THREAD)
|
||||
|
||||
// to use semaphore isolation
|
||||
HystrixCommandProperties.Setter().withExecutionIsolationStrategy(ExecutionIsolationStrategy.SEMAPHORE)
|
||||
```
|
||||
|
||||
线程池机制,每个 command 运行在一个线程中,限流是通过线程池的大小来控制的;信号量机制,command 是运行在调用线程中,通过信号量的容量来进行限流。
|
||||
|
||||
如何在线程池和信号量之间做选择?
|
||||
|
||||
**默认的策略**就是线程池。
|
||||
|
||||
**线程池**其实最大的好处就是对于网络访问请求,如果有超时的话,可以避免调用线程阻塞住。
|
||||
|
||||
而使用信号量的场景,通常是针对超大并发量的场景下,每个服务实例每秒都几百的 `QPS`,那么此时你用线程池的话,线程一般不会太多,可能撑不住那么高的并发,如果要撑住,可能要耗费大量的线程资源,那么就是用信号量,来进行限流保护。一般用信号量常见于那种基于纯内存的一些业务逻辑服务,而不涉及到任何网络访问请求。
|
||||
|
||||
### command key & command group
|
||||
我们使用线程池隔离,要怎么对**依赖服务**、**依赖服务接口**、**线程池**三者做划分呢?
|
||||
|
||||
每一个 command,都可以设置一个自己的名称 command key,同时可以设置一个自己的组 command group。
|
||||
```java
|
||||
private static final Setter cachedSetter = Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
|
||||
.andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorld"));
|
||||
|
||||
public CommandHelloWorld(String name) {
|
||||
super(cachedSetter);
|
||||
this.name = name;
|
||||
}
|
||||
```
|
||||
|
||||
command group 是一个非常重要的概念,默认情况下,就是通过 command group 来定义一个线程池的,而且还会通过 command group 来聚合一些监控和报警信息。同一个 command group 中的请求,都会进入同一个线程池中。
|
||||
|
||||
### command thread pool
|
||||
ThreadPoolKey 代表了一个 HystrixThreadPool,用来进行统一监控、统计、缓存。默认的 ThreadPoolKey 就是 command group 的名称。每个 command 都会跟它的 ThreadPoolKey 对应的 ThreadPool 绑定在一起。
|
||||
|
||||
如果不想直接用 command group,也可以手动设置 ThreadPool 的名称。
|
||||
```java
|
||||
private static final Setter cachedSetter = Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup"))
|
||||
.andCommandKey(HystrixCommandKey.Factory.asKey("HelloWorld"))
|
||||
.andThreadPoolKey(HystrixThreadPoolKey.Factory.asKey("HelloWorldPool"));
|
||||
|
||||
public CommandHelloWorld(String name) {
|
||||
super(cachedSetter);
|
||||
this.name = name;
|
||||
}
|
||||
```
|
||||
|
||||
### command key & command group & command thread pool
|
||||
**command key** ,代表了一类 command,一般来说,代表了底层的依赖服务的一个接口。
|
||||
|
||||
**command group** ,代表了某一个底层的依赖服务,这是很合理的,一个依赖服务可能会暴露出来多个接口,每个接口就是一个 command key。command group 在逻辑上去组织起来一堆 command key 的调用、统计信息、成功次数、timeout 超时次数、失败次数等,可以看到某一个服务整体的一些访问情况。一般来说,**推荐**根据一个服务区划分出一个线程池,command key 默认都是属于同一个线程池的。
|
||||
|
||||
比如说你以一个服务为粒度,估算出来这个服务每秒的所有接口加起来的整体 `QPS` 在 100 左右,你调用这个服务,当前这个服务部署了 10 个服务实例,每个服务实例上,其实用这个 command group 对应这个服务,给一个线程池,量大概在 10 个左右就可以了,你对整个服务的整体的访问 QPS 就大概在每秒 100 左右。
|
||||
|
||||
但是,如果说 command group 对应了一个服务,而这个服务暴露出来的几个接口,访问量很不一样,差异非常之大。你可能就希望在这个服务 command group 内部,包含的对应多个接口的 command key,做一些细粒度的资源隔离。就是说,对同一个服务的不同接口,使用不同的线程池。
|
||||
|
||||
```
|
||||
command key -> command group
|
||||
|
||||
command key -> 自己的 thread pool key
|
||||
```
|
||||
|
||||
逻辑上来说,多个 command key 属于一个command group,在做统计的时候,会放在一起统计。每个 command key 有自己的线程池,每个接口有自己的线程池,去做资源隔离和限流。
|
||||
|
||||
说白点,就是说如果你的 command key 要用自己的线程池,可以定义自己的 thread pool key,就 ok 了。
|
||||
|
||||
### coreSize
|
||||
设置线程池的大小,默认是 10。一般来说,用这个默认的 10 个线程大小就够了。
|
||||
```java
|
||||
HystrixThreadPoolProperties.Setter().withCoreSize(int value);
|
||||
```
|
||||
|
||||
### queueSizeRejectionThreshold
|
||||
如果说线程池中的 10 个线程都在工作中,没有空闲的线程来做其它的事情,此时再有请求过来,会先进入队列积压。如果说队列积压满了,再有请求过来,就直接 reject,拒绝请求,执行 fallback 降级的逻辑,快速返回。
|
||||
|
||||
![hystrix-thread-pool-queue](/img/hystrix-thread-pool-queue.png)
|
||||
|
||||
控制 queue 满了之后 reject 的 threshold,因为 maxQueueSize 不允许热修改,因此提供这个参数可以热修改,控制队列的最大大小。
|
||||
|
||||
```java
|
||||
HystrixThreadPoolProperties.Setter().withQueueSizeRejectionThreshold(int value);
|
||||
```
|
||||
|
||||
### execution.isolation.semaphore.maxConcurrentRequests
|
||||
设置使用 SEMAPHORE 隔离策略的时候允许访问的最大并发量,超过这个最大并发量,请求直接被 reject。
|
||||
|
||||
这个并发量的设置,跟线程池大小的设置,应该是类似的,但是基于信号量的话,性能会好很多,而且 Hystrix 框架本身的开销会小很多。
|
||||
|
||||
默认值是 10,尽量设置的小一些,因为一旦设置的太大,而且有延时发生,可能瞬间导致 tomcat 本身的线程资源被占满。
|
||||
|
||||
```java
|
||||
HystrixCommandProperties.Setter().withExecutionIsolationSemaphoreMaxConcurrentRequests(int value);
|
||||
```
|
@ -58,11 +58,6 @@ public class LocationCache {
|
||||
|
||||
写一个 GetCityNameCommand,策略设置为**信号量**。run() 方法中获取本地缓存。我们目的就是对获取本地缓存的代码进行资源隔离。
|
||||
```java
|
||||
/**
|
||||
* @author bingo
|
||||
* @since 2018/12/29
|
||||
*/
|
||||
|
||||
public class GetCityNameCommand extends HystrixCommand<String> {
|
||||
|
||||
private Long cityId;
|
||||
|
BIN
docs/high-availability/img/hystrix-thread-pool-queue.png
Normal file
BIN
docs/high-availability/img/hystrix-thread-pool-queue.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.1 KiB |
@ -10,7 +10,7 @@ es 在数据量很大的情况下(数十亿级别)如何提高查询效率
|
||||
说实话,es 性能优化是没有什么银弹的,啥意思呢?就是**不要期待着随手调一个参数,就可以万能的应对所有的性能慢的场景**。也许有的场景是你换个参数,或者调整一下语法,就可以搞定,但是绝对不是所有场景都可以这样。
|
||||
|
||||
### 性能优化的杀手锏——filesystem cache
|
||||
你往es里写的数据,实际上都写到磁盘文件里去了,查询的时候,操作系统会将磁盘文件里的数据自动缓存到 `filesystem cache` 里面去。
|
||||
你往 es 里写的数据,实际上都写到磁盘文件里去了,查询的时候,操作系统会将磁盘文件里的数据自动缓存到 `filesystem cache` 里面去。
|
||||
|
||||
![es-search-process](/img/es-search-process.png)
|
||||
|
||||
|
@ -34,11 +34,11 @@ channel.txCommit
|
||||
|
||||
但是问题是,RabbitMQ 事务机制(同步)一搞,基本上**吞吐量会下来,因为太耗性能**。
|
||||
|
||||
所以一般来说,如果你要确保说写 RabbitMQ 的消息别丢,可以开启`confirm`模式,在生产者那里设置开启`confirm`模式之后,你每次写的消息都会分配一个唯一的 id,然后如果写入了 RabbitMQ 中,RabbitMQ 会给你回传一个`ack`消息,告诉你说这个消息 ok 了。如果 RabbitMQ 没能处理这个消息,会回调你一个`nack`接口,告诉你这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息 id 的状态,如果超过一定时间还没接收到这个消息的回调,那么你可以重发。
|
||||
所以一般来说,如果你要确保说写 RabbitMQ 的消息别丢,可以开启 `confirm` 模式,在生产者那里设置开启 `confirm` 模式之后,你每次写的消息都会分配一个唯一的 id,然后如果写入了 RabbitMQ 中,RabbitMQ 会给你回传一个 `ack` 消息,告诉你说这个消息 ok 了。如果 RabbitMQ 没能处理这个消息,会回调你的一个 `nack` 接口,告诉你这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息 id 的状态,如果超过一定时间还没接收到这个消息的回调,那么你可以重发。
|
||||
|
||||
事务机制和`cnofirm`机制最大的不同在于,**事务机制是同步的**,你提交一个事务之后会**阻塞**在那儿,但是`confirm`机制是**异步**的,你发送个消息之后就可以发送下一个消息,然后那个消息RabbitMQ 接收了之后会异步回调你一个接口通知你这个消息接收到了。
|
||||
事务机制和 `cnofirm` 机制最大的不同在于,**事务机制是同步的**,你提交一个事务之后会**阻塞**在那儿,但是 `confirm` 机制是**异步**的,你发送个消息之后就可以发送下一个消息,然后那个消息 RabbitMQ 接收了之后会异步回调你的一个接口通知你这个消息接收到了。
|
||||
|
||||
所以一般在生产者这块**避免数据丢失**,都是用`confirm`机制的。
|
||||
所以一般在生产者这块**避免数据丢失**,都是用 `confirm` 机制的。
|
||||
|
||||
#### RabbitMQ 弄丢了数据
|
||||
就是 RabbitMQ 自己弄丢了数据,这个你必须**开启 RabbitMQ 的持久化**,就是消息写入之后会持久化到磁盘,哪怕是 RabbitMQ 自己挂了,**恢复之后会自动读取之前存储的数据**,一般数据不会丢。除非极其罕见的是,RabbitMQ 还没持久化,自己就挂了,**可能导致少量数据丢失**,但是这个概率较小。
|
||||
@ -46,20 +46,20 @@ channel.txCommit
|
||||
设置持久化有**两个步骤**:
|
||||
|
||||
- 创建 queue 的时候将其设置为持久化<br>
|
||||
这样就可以保证 RabbitMQ 持久化 queue 的元数据,但是不会持久化 queue 里的数据。
|
||||
这样就可以保证 RabbitMQ 持久化 queue 的元数据,但是它是不会持久化 queue 里的数据的。
|
||||
- 第二个是发送消息的时候将消息的 `deliveryMode` 设置为 2<br>
|
||||
就是将消息设置为持久化的,此时 RabbitMQ 就会将消息持久化到磁盘上去。
|
||||
|
||||
必须要同时设置这两个持久化才行,RabbitMQ 哪怕是挂了,再次重启,也会从磁盘上重启恢复 queue,恢复这个 queue 里的数据。
|
||||
|
||||
持久化可以跟生产者那边的`confirm`机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者`ack`了,所以哪怕是在持久化到磁盘之前,RabbitMQ 挂了,数据丢了,生产者收不到`ack`,你也是可以自己重发的。
|
||||
|
||||
注意,哪怕是你给 RabbitMQ 开启了持久化机制,也有一种可能,就是这个消息写到了 RabbitMQ 中,但是还没来得及持久化到磁盘上,结果不巧,此时 RabbitMQ 挂了,就会导致内存里的一点点数据丢失。
|
||||
|
||||
所以,持久化可以跟生产者那边的 `confirm` 机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者 `ack` 了,所以哪怕是在持久化到磁盘之前,RabbitMQ 挂了,数据丢了,生产者收不到 `ack`,你也是可以自己重发的。
|
||||
|
||||
#### 消费端弄丢了数据
|
||||
RabbitMQ 如果丢失了数据,主要是因为你消费的时候,**刚消费到,还没处理,结果进程挂了**,比如重启了,那么就尴尬了,RabbitMQ 认为你都消费了,这数据就丢了。
|
||||
|
||||
这个时候得用 RabbitMQ 提供的`ack`机制,简单来说,就是你关闭 RabbitMQ 的自动`ack`,可以通过一个 api 来调用就行,然后每次你自己代码里确保处理完的时候,再在程序里`ack`一把。这样的话,如果你还没处理完,不就没有`ack`?那 RabbitMQ 就认为你还没处理完,这个时候 RabbitMQ 会把这个消费分配给别的 consumer 去处理,消息是不会丢的。
|
||||
这个时候得用 RabbitMQ 提供的 `ack` 机制,简单来说,就是你必须关闭 RabbitMQ 的自动 `ack`,可以通过一个 api 来调用就行,然后每次你自己代码里确保处理完的时候,再在程序里 `ack` 一把。这样的话,如果你还没处理完,不就没有 `ack` 了?那 RabbitMQ 就认为你还没处理完,这个时候 RabbitMQ 会把这个消费分配给别的 consumer 去处理,消息是不会丢的。
|
||||
|
||||
![rabbitmq-message-lose-solution](/img/rabbitmq-message-lose-solution.png)
|
||||
|
||||
@ -68,7 +68,7 @@ RabbitMQ 如果丢失了数据,主要是因为你消费的时候,**刚消费
|
||||
#### 消费端弄丢了数据
|
||||
唯一可能导致消费者弄丢数据的情况,就是说,你消费到了这个消息,然后消费者那边**自动提交了 offset**,让 Kafka 以为你已经消费好了这个消息,但其实你才刚准备处理这个消息,你还没处理,你自己就挂了,此时这条消息就丢咯。
|
||||
|
||||
这不是跟 RabbitMQ 差不多吗,大家都知道 Kafka 会自动提交 offset,那么只要**关闭自动提交** offset,在处理完之后自己手动提交 offset,就可以保证数据不会丢。但是此时确实还是**可能会有重复消费**,比如你刚处理完,还没提交offset,结果自己挂了,此时肯定会重复消费一次,自己保证幂等性就好了。
|
||||
这不是跟 RabbitMQ 差不多吗,大家都知道 Kafka 会自动提交 offset,那么只要**关闭自动提交** offset,在处理完之后自己手动提交 offset,就可以保证数据不会丢。但是此时确实还是**可能会有重复消费**,比如你刚处理完,还没提交 offset,结果自己挂了,此时肯定会重复消费一次,自己保证幂等性就好了。
|
||||
|
||||
生产环境碰到的一个问题,就是说我们的 Kafka 消费者消费到了数据之后是写到一个内存的 queue 里先缓冲一下,结果有的时候,你刚把消息写入内存 queue,然后消费者会自动提交 offset。然后此时我们重启了系统,就会导致内存 queue 里还没来得及处理的数据就丢失了。
|
||||
|
||||
|
@ -90,7 +90,7 @@ A 系统处理完了直接返回成功了,人都以为你这个请求就成功
|
||||
| topic 数量对吞吐量的影响 | | | topic 可以达到几百/几千的级别,吞吐量会有较小幅度的下降,这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的 topic | topic 从几十到几百个时候,吞吐量会大幅度下降,在同等机器下,Kafka 尽量保证 topic 数量不要过多,如果要支撑大规模的 topic,需要增加更多的机器资源 |
|
||||
| 时效性 | ms 级 | 微秒级,这是 RabbitMQ 的一大特点,延迟最低 | ms 级 | 延迟在 ms 级以内 |
|
||||
| 可用性 | 高,基于主从架构实现高可用 | 同 ActiveMQ | 非常高,分布式架构 | 非常高,分布式,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用 |
|
||||
| 消息可靠性 | 有较低的概率丢失数据 | | 经过参数优化配置,可以做到 0 丢失 | 同 RocketMQ |
|
||||
| 消息可靠性 | 有较低的概率丢失数据 | 基本不丢 | 经过参数优化配置,可以做到 0 丢失 | 同 RocketMQ |
|
||||
| 功能支持 | MQ 领域的功能极其完备 | 基于 erlang 开发,并发能力很强,性能极好,延时很低 | MQ 功能较为完善,还是分布式的,扩展性好 | 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用 |
|
||||
|
||||
|
||||
|
BIN
img/hystrix-thread-pool-queue.png
Normal file
BIN
img/hystrix-thread-pool-queue.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 5.1 KiB |
Loading…
Reference in New Issue
Block a user