From 4fe4d73eb0609a0e1933d86b092c76e6a7ba6d25 Mon Sep 17 00:00:00 2001 From: yanglbme Date: Sat, 29 Dec 2018 19:14:21 +0800 Subject: [PATCH] docs: add hystrix-semphore-isolation, fix #12 - Add hystrix-semphore-isolation.md - Update RabbitMQ, fix #12 - Fix typo --- README.md | 1 + .../hystrix-execution-isolation.md | 106 ++++++++++++++++++ .../hystrix-semphore-isolation.md | 5 - .../img/hystrix-thread-pool-queue.png | Bin 0 -> 5221 bytes .../es-optimizing-query-performance.md | 2 +- ...e-the-reliable-transmission-of-messages.md | 16 +-- docs/high-concurrency/why-mq.md | 2 +- img/hystrix-thread-pool-queue.png | Bin 0 -> 5221 bytes 8 files changed, 117 insertions(+), 15 deletions(-) create mode 100644 docs/high-availability/hystrix-execution-isolation.md create mode 100644 docs/high-availability/img/hystrix-thread-pool-queue.png create mode 100644 img/hystrix-thread-pool-queue.png diff --git a/README.md b/README.md index ca92962..3ef8c08 100644 --- a/README.md +++ b/README.md @@ -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) ### 高可用系统 - 如何设计一个高可用系统? diff --git a/docs/high-availability/hystrix-execution-isolation.md b/docs/high-availability/hystrix-execution-isolation.md new file mode 100644 index 0000000..fc58e95 --- /dev/null +++ b/docs/high-availability/hystrix-execution-isolation.md @@ -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); +``` \ No newline at end of file diff --git a/docs/high-availability/hystrix-semphore-isolation.md b/docs/high-availability/hystrix-semphore-isolation.md index 63e03b5..d0c6e47 100644 --- a/docs/high-availability/hystrix-semphore-isolation.md +++ b/docs/high-availability/hystrix-semphore-isolation.md @@ -58,11 +58,6 @@ public class LocationCache { 写一个 GetCityNameCommand,策略设置为**信号量**。run() 方法中获取本地缓存。我们目的就是对获取本地缓存的代码进行资源隔离。 ```java -/** - * @author bingo - * @since 2018/12/29 - */ - public class GetCityNameCommand extends HystrixCommand { private Long cityId; diff --git a/docs/high-availability/img/hystrix-thread-pool-queue.png b/docs/high-availability/img/hystrix-thread-pool-queue.png new file mode 100644 index 0000000000000000000000000000000000000000..08b3b73f81cd5a5af89756dac775653bc789956b GIT binary patch literal 5221 zcmd5=_fu0{w7nokL_m-bdRIj0ND~MMQluAYDjf~INe{huP*4Pdl%Rl-rZlB@K6)=w zKB^QU^d|Kk{)0F3y&vA4$(%cr+`G@-XYIAtPL!605*Z0S2?Rl8%1`8Vz*+`D1S-U^ z%R6@WO|V>de)7x}f^JYUyQ)rvtQfM=sj8$7%!TIkBG=u}QEm zUu=xH1oCb@(cw^2DOD~T5RT&#pxxkr$q=O9uQG7cs{Sc#o68nwpxY$7-C{ zV1=dEsW`ph2tP=}-j_`_>n2fIW>%TJ{@B2*AEu?}UQB^4NHy*~IX>PPabS%Su)dPgvqZ0;T_IZ9 zHJYBzSnYU4IaHQGUi%%V<~?1#->cym+A>*t(a8kpKK)-E1#fTfePVr_C@vXMJL(}O9_GaLYN*sNa&BW z0sPL{j_7Nu5YZHhCim!pCK{uBI?@Rg4gkxi*s`>XWEFMxiZX zG(-s5Yr_?2k)fnNe20;?&dyt1a*^Ut2;?S)&g$wVD7hcKQ~t|d12!v+W~~)m8GQ;K zCSjdeku5T>Znf~HuQVGTCTVPJ91>BdqPnXAN5m-T>gz{WV84IYG2}rAy3EUG+oTbb zkffpZ78D%Pb8URUFA*`VEB=_5qL{E;8@Zt}#vB&@+HITaQ3fLD?Bq}k9nU#}aVUA~ zN*g3wY2pd}z*kJhX5=V;l=Lh*DJeWH!7;$pbSklYG(;ppFovApIcjt z3AuKYUi{ffdls1xs+-oZQ2*Wgk-FcW8XDxe>X`8Lb$f9}@{l~cdVnne^#0GPH0ekm zM0)~D`#_G8p#P~C5fKq92gkyM*rPa$e%KQ=wU(ABs%$P2^x;MvHMc%1A0Ihq(slm2 zBX<2_lenFo*E#L>AC8#0VUclht&x-*&YQJ|^Fx|4VbhN7!(W~PLd#|2;URUZKNAHH zOy?HAoe z|M7hn1%<2Y9+J{Wvmr2Ud^i~cs>OP6mU053qoa#VNNBI#tcz)AXn>i%D7U5V?Ci|K z-7g-PgMk1QzJFgcO8e5l4xdpH*;jx~-`|`C}zeM$G!Hf54Bp;uL-?jMmc7uw@M)9<4sEfyISw*=6E2e}5tCK-@Lfj&1OA>r)8LI^pN1T7K~IKi~G?8hySQ-BWCGenGP@Hi19*3^XBcFI9~k90C=m+7=fNk5nO7=f@uW z-OwXR0fBy*J9k>M$Lxh>%kgf2AHwSE1E9b+cI(IP`uh55%nv$3m-}JP5+7eYFL|MQ zkj)KbBp+90K&(C5&A03aTH%lA8+pKI-6+>MEY}OnuUB>&8u7-JLART-$go^0NqKM_ zZpW=zopgd#6oXfSsN;OUU~6imR5!g}0!q^{H1tv?9vCmBoxE}52LGz`o7=QyL22Un z+0{08AWJ&%S9b$(XbcXn&>?KgOb6np_AK3p=-e~8qZ9qT8Gc5|GN=*HNwiwe-QXK0 zg*lXfvs}+V8Gk8+w{|qeddLoy;nQ**9-ztTyy%e-0aU>i-E?@bhG1fUXCNypDUAKkA+QM!_m?4hju_fwR8lk+aUqV#SwRU@>%@10H#MV;SmG~BaS)Sk0m>*sO7&H zmSYJ3(Z*G`DrrxNk6icf1HxwX^??IfToVpB_kD>v_k36taB)ThP7H!>-@a{jcDzp( zTwIL%GhuKh@p_RMG(G*(kXVG<-&Z_4P6p4wEoFJ(qSSPgY@JcnpHKX=A#2Ap7w(C=f97;2^!p zeKg=oD3IU^jP>C|I*gbT3@Uh0*-I^~<@4@hf6UbUn;``aO$48D)r#BZBqt1?3&aU( zuuMRP-+P*C)}JLiUT#O@>+1{D0F9P7-dhc+-l)29I1?aqe1MMu`LPKIP^c#JxU^0& zMl=V-#*)S|Ng|5t&#UZCdOu3O#b9ngDZ)0ln7w8RURqf#{9Vqhf9P4kWZv(nqORV0 zStOT*w#L4`o0~Pe@YY{u`9slwrF?QN5nFK=IR;mnms zVi}h%M%X+whK4_SceyX-Zlmx8~49Z*ne@GSx;1)RTjFOe3BE;FpK zPo)0}hUP3faYi_6O~ot-K`g61dk&hfFdf^x&d!&6%) zzv$g(ZgOA%$*Tk1>`LZsF71Bs~sdPPB7%h)*Xn`JxJsxKoShifV^ zZ-$_14}35V=XG&*C}`P6T64P71=;qsYkrg%nawAMa+DI<(jU5}r8hfP=3ude(b$He zq!|rqLPy|O9_#DlF?y}7t>!?ZiUQ8X9412N=PiJEnxCKk-P+l?jG(2j9}a9_Lwox* zkOC`$$1sq^|A-)3$ET+oAo){nZc}s4oTPrh@oL`6STT!*g+)=?LKlC13yrC75Mcb7 zMV~2Y0c)}Sm4Pe8H9I87`)d*YXUEx^nbJkYNFZ`YOBg3DBO}tJj?ka3%kxt|UB^qz zue8O|g?##STk8C10gO728s>FE_1{Ty`1#4%-qDu&gcA9w8wn>nc|%d4;}?=NrGEw;zHzf!9P%*0;c&KYKHlCxxp;XUN*o%4ukzMS13;5t zdOVCvSHm)xyw^(KR!PwHyQ>_8B8vbx*t2_SlmGjW1gMJN*WgoMx1|Hd>HJaLMw?}My_^IYx z`IyNSC^R&bRYIbwG2kXr_axVRxi3>mO^r03MP_5vQeLbh&OdpfDeMNjl#~%860J+j z){?KCBd@Fb4h+0TRc@=eGcm_0E>K_vW#uSwm-*1h*jP9?D!-h{TF*@rN=iyLau#VG zb`Fl#KADTD760w+ZFWh?7r>#rtd9$Vjt0rH3k&N4s)`vN)_t3j!Ua&r#e<09;^Lz4 zIasGmkUq8!p;L;bbs2Gp2n+iut)Z$)1yqEBJR;}!&3=)xFsp}Np+(CWr@F>BY>x%Z z8X%|v)FK=_nBQ^sR7YoPP?i6Z@D8V3Z?+|{Brr)9`B!gb2AtE~K=3pSepVH<9~aVsi*n;?1W+6ARumk=Xck)S6ztEh=_$S8WZQjZ_@66;OM=TAnhE*Y^RwzVp39h zFE4Q^DXHgWZB@M~f|go(dSS-Z9_2@UW!HQsiZZ&pyJ7WYLe8_7bR|}``FD5OWvQEP z3dmrQLkNsa4s=4uS>4&ei-KpRuuD#_^+oy~IoBNi>vQRQ=jZ1IKtlq8>Y`thz~Mk| z5T>T4Uh*R*!EmxUi=UgDyBRofj1iIBXV3`*kYoLs$5=&?arHb%F9?iq%F5UE3k-vT zg9!n^5dZ>;-rf(valC#1J`Cuq1;9$m11s{up`lB+5Bl{MAoxqX{p)vVoR0$-4hQI$ z(}vqm{h$Ps+g;6!%Xcz+FH&}A%COv&ITb+lWyFD_8{+{(jnfthht`P@+V3(F8eLU= z^BJZE?30)Gs#bM$(Vai>9=PL~XBZr==tCu7Ng^;T+W%YgvtwqLLEM>D)alwl%LMeh?r z0F0z`LPXGW`0o2XBTKU33{m&h!K)@FCWRU0g%)%})YTnEB;vSN%iF5rM{HYX8$+fuDtRsX_vIB7=D?)dGXBkyV7N^y z@0{?Y%Ke`QR=q9XUv&vMPKh5K9nFpynAR@Ioi44Ex6$V#d&WzxZi2aW>7d4eGNj0= z^W8nKrBo~D`IhToOcgeKG?Nm~Bx$id-#X^5TclYmGWzAqm+@L3F@Qw@RCfx4kmNKZ zW{KPbIA{UGs8i=10(((RWK@*R_VE%1Y_Xs~EtWw{!?17^agmyAeWMs&aXy0+{j||o zJ^J?Dp0;N+>YE|yS~S1ke{%E+z{V9`O#X7Svl~;gW|NN}c%b!IF7mNr-y`||^)=jf aCKF{tD@ -这样就可以保证 RabbitMQ 持久化 queue 的元数据,但是不会持久化 queue 里的数据。 +这样就可以保证 RabbitMQ 持久化 queue 的元数据,但是它是不会持久化 queue 里的数据的。 - 第二个是发送消息的时候将消息的 `deliveryMode` 设置为 2
就是将消息设置为持久化的,此时 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 里还没来得及处理的数据就丢失了。 diff --git a/docs/high-concurrency/why-mq.md b/docs/high-concurrency/why-mq.md index 4aef272..edb3ad0 100644 --- a/docs/high-concurrency/why-mq.md +++ b/docs/high-concurrency/why-mq.md @@ -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 功能,在大数据领域的实时计算以及日志采集被大规模使用 | diff --git a/img/hystrix-thread-pool-queue.png b/img/hystrix-thread-pool-queue.png new file mode 100644 index 0000000000000000000000000000000000000000..08b3b73f81cd5a5af89756dac775653bc789956b GIT binary patch literal 5221 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 aCKF{tD@