diff --git a/README.md b/README.md index 672c34eb513263d42f72dc1648b2eac674c49147..97f06ce449ebd9fe2857a2fc9e41f3f58615d6c9 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,16 @@ # 互联网 Java 工程师进阶知识完全扫盲 - [![license](https://img.shields.io/badge/license-Attribution--NonCommercial%204.0%20-brightgreen.svg)](https://github.com/doocs/advanced-java/blob/master/LICENSE) [![original](https://img.shields.io/badge/original-%E4%B8%AD%E5%8D%8E%E7%9F%B3%E6%9D%89-orange.svg)](https://github.com/doocs/advanced-java) [![stars](https://img.shields.io/github/stars/doocs/advanced-java.svg)](https://github.com/doocs/advanced-java/stargazers) [![forks](https://img.shields.io/github/forks/doocs/advanced-java.svg)](https://github.com/doocs/advanced-java/network/members) [![issues](https://img.shields.io/github/issues/doocs/advanced-java.svg)](https://github.com/doocs/advanced-java/issues) -[![PRs Welcome](https://img.shields.io/badge/PRs-Welcome-brightgreen.svg)](http://makeapullrequest.com) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](http://makeapullrequest.com) 本系列知识出自中华石杉,我对这部分知识做了一个系统的整理,方便学习查阅。By the way,微信公众号**石杉的架构笔记**(id:shishan100)有其它很多架构知识,墙裂推荐~ 一点小建议:学习本系列知识之前,如果你完全没接触过 `MQ`、`ES`、`Redis`、`Dubbo`、`Hystrix` 等,那么我建议你可以先在网上搜一下每一块知识的快速入门,跟着入门 Demo [玩](https://doocs.github.io/advanced-java/#/offer)一下,然后再开始每一块知识的学习,这样效果更好噢~ ## 高并发架构 - ### [消息队列](/docs/high-concurrency/mq-interview.md) - [为什么使用消息队列?消息队列有什么优点和缺点?Kafka、ActiveMQ、RabbitMQ、RocketMQ 都有什么优点和缺点?](/docs/high-concurrency/why-mq.md) - [如何保证消息队列的高可用?](/docs/high-concurrency/how-to-ensure-high-availability-of-message-queues.md) @@ -54,7 +52,6 @@ - [如何设计一个高并发系统?](/docs/high-concurrency/high-concurrency-design.md) ## 分布式系统 - ### [面试连环炮](/docs/distributed-system/distributed-system-interview.md) ### 系统拆分 - [为什么要进行系统拆分?如何进行系统拆分?拆分后不用 Dubbo 可以吗?](/docs/distributed-system/why-dubbo.md) diff --git a/docs/distributed-system/dubbo-rpc-design.md b/docs/distributed-system/dubbo-rpc-design.md index 5feafb5268c6c1ff644ed364c6c0f4e31a3847e2..f47ce8083cd1db1048f7073cef6e3801e12a5919 100644 --- a/docs/distributed-system/dubbo-rpc-design.md +++ b/docs/distributed-system/dubbo-rpc-design.md @@ -1,5 +1,5 @@ ## 面试题 -如何自己设计一个类似dubbo的rpc框架? +如何自己设计一个类似 Dubbo 的 RPC 框架? ## 面试官心理分析 说实话,就这问题,其实就跟问你如何自己设计一个 MQ 一样的道理,就考两个: diff --git a/docs/high-availability/hystrix-thread-pool-current-limiting.md b/docs/high-availability/hystrix-thread-pool-current-limiting.md index 999482195e3e5ee6345ccf0c393b5baae814492a..1084687192531dbd2510ef51b584c83f7fad99cd 100644 --- a/docs/high-availability/hystrix-thread-pool-current-limiting.md +++ b/docs/high-availability/hystrix-thread-pool-current-limiting.md @@ -10,10 +10,154 @@ Hystrix 通过判断线程池或者信号量是否已满,超出容量的请求 ### 线程池隔离技术的设计 Hystrix 采用了 Bulkhead Partition 舱壁隔离技术,来将外部依赖进行资源隔离,进而避免任何外部依赖的故障导致本服务崩溃。 -> **舱壁隔离**,是说将船体内部空间区隔划分成若干个隔舱,一旦某几个隔舱发生破损进水,水流不会在其间相互流动,如此一来船舶在受损时,依然能具有足够的浮力和稳定性,进而减低立即沉船的危险。 +**舱壁隔离**,是说将船体内部空间区隔划分成若干个隔舱,一旦某几个隔舱发生破损进水,水流不会在其间相互流动,如此一来船舶在受损时,依然能具有足够的浮力和稳定性,进而减低立即沉船的危险。 ![bulkhead-partition](/img/bulkhead-partition.jpg) Hystrix 对每个外部依赖用一个单独的线程池,这样的话,如果对那个外部依赖调用延迟很严重,最多就是耗尽那个依赖自己的线程池而已,不会影响其他的依赖调用。 ### Hystrix 应用线程池机制的场景 +- 每个服务都会调用几十个后端依赖服务,那些后端依赖服务通常是由很多不同的团队开发的。 +- 每个后端依赖服务都会提供它自己的 client 调用库,比如说用 thrift 的话,就会提供对应的 thrift 依赖。 +- client 调用库随时会变更。 +- client 调用库随时可能会增加新的网络请求的逻辑。 +- client 调用库可能会包含诸如自动重试、数据解析、内存中缓存等逻辑。 +- client 调用库一般都对调用者来说是个黑盒,包括实现细节、网络访问、默认配置等等。 +- 在真实的生产环境中,经常会出现调用者,突然间惊讶的发现,client 调用库发生了某些变化。 +- 即使 client 调用库没有改变,依赖服务本身可能有会发生逻辑上的变化。 +- 有些依赖的 client 调用库可能还会拉取其他的依赖库,而且可能那些依赖库配置的不正确。 +- 大多数网络请求都是同步调用的。 +- 调用失败和延迟,也有可能会发生在 client 调用库本身的代码中,不一定就是发生在网络请求中。 + +简单来说,就是你必须默认 client 调用库很不靠谱,而且随时可能发生各种变化,所以就要用强制隔离的方式来确保任何服务的故障不会影响当前服务。 + +### 线程池机制的优点 +- 任何一个依赖服务都可以被隔离在自己的线程池内,即使自己的线程池资源填满了,也不会影响任何其他的服务调用。 +- 服务可以随时引入一个新的依赖服务,因为即使这个新的依赖服务有问题,也不会影响其他任何服务的调用。 +- 当一个故障的依赖服务重新变好的时候,可以通过清理掉线程池,瞬间恢复该服务的调用,而如果是 tomcat 线程池被占满,再恢复就很麻烦。 +- 如果一个 client 调用库配置有问题,线程池的健康状况随时会报告,比如成功/失败/拒绝/超时的次数统计,然后可以近实时热修改依赖服务的调用配置,而不用停机。 +- 基于线程池的异步本质,可以在同步的调用之上,构建一层异步调用层。 + +简单来说,最大的好处,就是资源隔离,确保说任何一个依赖服务故障,不会拖垮当前的这个服务。 + +### 线程池机制的缺点 +- 线程池机制最大的缺点就是增加了 CPU 的开销。
+除了 tomcat 本身的调用线程之外,还有 hystrix 自己管理的线程池。 + +- 每个 command 的执行都依托一个独立的线程,会进行排队,调度,还有上下文切换。 +- Hystrix 官方自己做了一个多线程异步带来的额外开销,通过对比多线程异步调用+同步调用得出,Netflix API 每天通过 Hystrix 执行 10 亿次调用,每个服务实例有 40 个以上的线程池,每个线程池有 10 个左右的线程。)最后发现说,用 Hystrix 的额外开销,就是给请求带来了 3ms 左右的延时,最多延时在 10ms 以内,相比于可用性和稳定性的提升,这是可以接受的。 + +我们可以用 Hystrix semaphore 技术来实现对某个依赖服务的并发访问量的限制,而不是通过线程池/队列的大小来限制流量。 + +sempahore 技术可以用来限流和削峰,但是不能用来对调研延迟的服务进行 timeout 和隔离。 + +`execution.isolation.strategy` 设置为 `SEMAPHORE`,那么 Hystrix 就会用 semaphore 机制来替代线程池机制,来对依赖服务的访问进行限流。如果通过 semaphore 调用的时候,底层的网络调用延迟很严重,那么是无法 timeout 的,只能一直 block 住。一旦请求数量超过了 semephore 限定的数量之后,就会立即开启限流。 + +### 接口限流 Demo +假设一个线程池大小为 8,等待队列的大小为 10。timeout 时长我们设置长一些,20s。 + +在 command 内部,写死代码,做一个 sleep,比如 sleep 3s。 + +- withCoreSize:设置线程池大小 +- withMaxQueueSize:设置等待队列大小 +- withQueueSizeRejectionThreshold:这个与 withMaxQueueSize 配合使用,等待队列的大小,取得是这两个参数的较小值。 + +如果只设置了线程池大小,另外两个 queue 相关参数没有设置的话,等待队列是处于关闭的状态。 + +```java +public class GetProductInfoCommand extends HystrixCommand { + + private Long productId; + + private static final HystrixCommandKey KEY = HystrixCommandKey.Factory.asKey("GetProductInfoCommand"); + + public GetProductInfoCommand(Long productId) { + super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ProductInfoService")) + .andCommandKey(KEY) + // 线程池相关配置信息 + .andThreadPoolPropertiesDefaults(HystrixThreadPoolProperties.Setter() + // 设置线程池大小为8 + .withCoreSize(8) + // 设置等待队列大小为10 + .withMaxQueueSize(10) + .withQueueSizeRejectionThreshold(12)) + .andCommandPropertiesDefaults(HystrixCommandProperties.Setter() + .withCircuitBreakerEnabled(true) + .withCircuitBreakerRequestVolumeThreshold(20) + .withCircuitBreakerErrorThresholdPercentage(40) + .withCircuitBreakerSleepWindowInMilliseconds(3000) + // 设置超时时间 + .withExecutionTimeoutInMilliseconds(20000) + // 设置fallback最大请求并发数 + .withFallbackIsolationSemaphoreMaxConcurrentRequests(30))); + this.productId = productId; + } + + @Override + protected ProductInfo run() throws Exception { + System.out.println("调用接口查询商品数据,productId=" + productId); + + if (productId == -1L) { + throw new Exception(); + } + + // 请求过来,会在这里hang住3秒钟 + if (productId == -2L) { + TimeUtils.sleep(3); + } + + String url = "http://localhost:8081/getProductInfo?productId=" + productId; + String response = HttpClientUtils.sendGetRequest(url); + System.out.println(response); + return JSONObject.parseObject(response, ProductInfo.class); + } + + @Override + protected ProductInfo getFallback() { + ProductInfo productInfo = new ProductInfo(); + productInfo.setName("降级商品"); + return productInfo; + } +} +``` + +我们模拟 25 个请求。前 8 个请求,调用接口时会直接被 hang 住 3s,那么后面的 10 的请求会先进入等待队列中等待前面的请求执行完毕。最后的 7 个请求过来,会直接被 reject,调用 fallback 降级逻辑。 + +```java +@SpringBootTest +@RunWith(SpringRunner.class) +public class RejectTest { + + @Test + public void testReject() { + for (int i = 0; i < 25; ++i) { + new Thread(() -> HttpClientUtils.sendGetRequest("http://localhost:8080/getProductInfo?productId=-2")).start(); + } + // 防止主线程提前结束执行 + TimeUtils.sleep(50); + } +} +``` + +从执行结果中,我们可以明显看出一共打印出了 7 个降级商品。这也就是请求数超过线程池+队列的数量而直接被 reject 的结果。 + +```c +ProductInfo(id=null, name=降级商品, price=null, pictureList=null, specification=null, service=null, color=null, size=null, shopId=null, modifiedTime=null, cityId=null, cityName=null, brandId=null, brandName=null) +ProductInfo(id=null, name=降级商品, price=null, pictureList=null, specification=null, service=null, color=null, size=null, shopId=null, modifiedTime=null, cityId=null, cityName=null, brandId=null, brandName=null) +ProductInfo(id=null, name=降级商品, price=null, pictureList=null, specification=null, service=null, color=null, size=null, shopId=null, modifiedTime=null, cityId=null, cityName=null, brandId=null, brandName=null) +ProductInfo(id=null, name=降级商品, price=null, pictureList=null, specification=null, service=null, color=null, size=null, shopId=null, modifiedTime=null, cityId=null, cityName=null, brandId=null, brandName=null) +ProductInfo(id=null, name=降级商品, price=null, pictureList=null, specification=null, service=null, color=null, size=null, shopId=null, modifiedTime=null, cityId=null, cityName=null, brandId=null, brandName=null) +ProductInfo(id=null, name=降级商品, price=null, pictureList=null, specification=null, service=null, color=null, size=null, shopId=null, modifiedTime=null, cityId=null, cityName=null, brandId=null, brandName=null) +调用接口查询商品数据,productId=-2 +调用接口查询商品数据,productId=-2 +调用接口查询商品数据,productId=-2 +调用接口查询商品数据,productId=-2 +调用接口查询商品数据,productId=-2 +调用接口查询商品数据,productId=-2 +调用接口查询商品数据,productId=-2 +调用接口查询商品数据,productId=-2 +ProductInfo(id=null, name=降级商品, price=null, pictureList=null, specification=null, service=null, color=null, size=null, shopId=null, modifiedTime=null, cityId=null, cityName=null, brandId=null, brandName=null) +{"id": -2, "name": "iphone7手机", "price": 5599, "pictureList":"a.jpg,b.jpg", "specification": "iphone7的规格", "service": "iphone7的售后服务", "color": "红色,白色,黑色", "size": "5.5", "shopId": 1, "modifiedTime": "2017-01-01 12:00:00", "cityId": 1, "brandId": 1} +// 后面都是一些正常的商品信息,就不贴出来了 +//... +``` \ No newline at end of file