春の雲のコメント(4)Hystrix実装原理

1. Hystrixプロセス

次の図に示すパッケージHystrix依存サービス要求に使用されるフロー:

  1. ビルドHystrixCommandまたはHystrixObservableCommandオブジェクト
  2. ラン(すなわち、パッケージコマンドオブジェクトロジック上)
  3. キャッシュされた結果があります
  4. (回路と同様)要求ラインが開いている場合
  5. ときフル何が起こるかのためにプール/リクエストキュー/信号アカウントスレッド
  6. 使用HystrixObservableCommand.construct()またはHystrixCommand.run()
  7. 計算されたリンクの健康度
  8. 障害のフォールバックロジック
  9. 通常の応答に戻ります

1.2ビルドオブジェクト

最初のステップは、使用hystrix作成することであるHystrixCommandか、HystrixObservableCommandあなたが要求依存サービスを送信する必要があることを示すことを目的とします。あなたは、コンストラクタに任意のパラメータを渡すことができます。

唯一の目的のサービスにするたびに依存する場合、あなたは、単一の応答は構造が以下の返すHystrixCommandことができます。

HystrixObservableCommand command = new HystrixObservableCommand(arg1, arg2);

1.3ファイル名を指定して実行

Hystrixコマンドは、(4つの方法を提供しHystrixCommand、すべての4つの方法が、サポートHystrixObservableCommandパッケージを要求し実行するためにのみ、後者二つの支持体を):

  • execute()- ブロック、ときに依存するサービス応答(または例外/投げる)、リターン結果

  • queue()-戻り値Future非同期オブジェクトを返す結果によって得られたオブジェクト

  • observe()-返しObservable登録することで、サービス依存(または例外/捨てる)に応じて、オブジェクト、すぐに要求をSubscriber取得したリターン結果を

  • toObservable()-戻りObservable、オブジェクトが、サブスクリプションが要求するまでに発行されたオブジェクトのみ、およびサービス応答に依存している(または例外/残業を投げる)、登録されたことにより、Subscriberリターン結果を得ます

K             value   = command.execute();
Future<K>     fValue  = command.queue();
Observable<K> ohValue = command.observe();         // hot observable(注:调用observe()方法时,请求立即发出)
Observable<K> ocValue = command.toObservable();    // cold observable(注:只有在返回的ocValue上调用subscribe时,才会发出请求)

内部的には、execute()同期呼び出し、内部コールqueue().get()方法。queue()内部コールtoObservable().toBlocking().toFuture()つまり、HystrixCommand1つを介して内部Observable同期コマンドは、もともとこのような単純なロジックに応答を返すために使用された場合でも、その要求を実行します。

1.4結果キャッシュするかどうか

この機能が有効になっているリクエストキャッシュされた結果とキャッシュヒットした場合、キャッシュは、直ちに対応させていただきますObservableフォームオブジェクトに戻ります。

リクエストラインは1.5開いている場合

コマンドが実行されると、ヒューズ状態の哺乳類、最初のチェックは要求ラインが開いているかどうかを決定します

リクエストラインが開いている場合は、Hystrixは、このコマンドを実行しませんが、直接使用することは、「フォールバックロジックを失敗します。」

1.6スレッドプール/要求キュー/信号は、いつフル何が起こるかを占めて

(あなたがスレッドプールを使用しない場合、またはセマフォ)あなたが実行し、スレッドプールとキュー要求に関連付けられた現在のコマンドが必要な場合は、Hystrixは、このコマンドを実行しませんが、直接使用することは、「フォールバックロジックを失敗します。」

1.7を使用しますHystrixObservableCommand.construct()か、HystrixCommand.run()

Hystrixは依存サービスを要求するために内部的に使用するさまざまな方法のために、あなたのクラスに応じて使用します。

  • HystrixCommand.run()- 返品または例外応答を投げます

  • HystrixObservableCommand.construct()-戻り観測物体に応答して観察者に通知が到着する、またはコールバックonError異常通知する方法

場合run()またはconstruct()コマンドセットへのタイムアウトしきい値よりも時間のかかる方法で、リクエストのスレッドの実行がスローされますTimeoutException(コマンドは独自の呼び出し元のスレッド内で実行されていない場合は、別のタイマースレッドがスローされます)。この場合、Hystrixはロールバックロジックを実行することができないと返され、最終的な(コマンド実行スレッドが中断されていない場合)の応答を無視します。

コマンド自体は例外をスローし、いくつかのログを追加し、データ収集、ダイレクトリターン応答を監視した後、通常のHystrixに応答を返していない場合。Hystrixにおいて使用run()方法が、それはまだ内部Hystrixを生成するObservableオブジェクトを生成すると、単一の要求を返すonCompletedHystrixを使用中に、通知をconstruct()直接に戻ったときにconstruct()生成されたObservableオブジェクトを

1.8行の健全性を計算します

Hystrix要求が成功、失敗、拒否される、またはタイムアウト情報は、ヒューズに報告し、ヒューズが使用される統計データのためのいくつかのカウンタを維持します。

決意回路ヒューズ統計によると、回復期間の終わりには、健康に戻ってまだない場合には、これらのカウンタによって生成された統計は、回復期間の終了まで、特定の時点でのヒューズが、短絡がサービス後続の要求に頼ることができ作る、ヒューズは再びシャットダウンしますライン。

1.9フォールバックロジックの失敗

コマンドが失敗すると、Hystrixは、障害がが原因である可能性があり、ロジックが失敗したフォールバックします。

  • construct()またはrun()メソッドは例外をスローします
  • ラインが開いている場合は、コマンドが短絡されるようにします
  • スレッドプールまたは指令信号が充填される量に対応する場合
  • タイムアウト

障害のフォールバック・ロジックは、一般的な応答情報が含まれている、というかむしろ従属任意のネットワークに起因するよりも、メモリキャッシュから他の固定論理に対応するためにそれらを取得します。要求がネットワーク必見のロールバック・ロジックの障害が含まれている場合、ネットワークは、別の要求にパッケージ化する必要がありますHystrixCommandHystrixObservableCommand

使用するときはHystrixCommand、時間を、の実現を通じてHystrixCommand.getFallback()戻ったときに、ロールバック応答しません。

使用している場合HystrixObservableCommandの実現を通じて、時間をHystrixObservableCommand.resumeWithFallback()知らせるために戻って観察可能なオブジェクトのオブザーバーは、いつロールバック応答しません。

障害フォールバック方式が応答を返す場合、Hystrixこの応答は、コマンドの呼び出し元に返されます。内部Hystrix呼び出した場合HystrixCommand.getFallback()、それは、被監視オブジェクトを作成し、パッケージのユーザーが達成getFallback()レスポンス方式のリターンを、内部Hystrix呼び出した場合HystrixObservableCommand.resumeWithFallback()、ユーザーが実現しますresumeWithFallback()観察可能オブジェクトが直接戻さ。

あなたがフォールバック方式、または失敗のフォールバック方式を実現するために失敗しない場合は例外をスローし、または内部Hystrix観察可能なオブジェクトを生成しますが、それは任意の応答を生成しない、とすることによってonError停止要求の即時通知。デフォルトでHystrixはonError異常は、発信者に通知しているが発生しました。あなたは、障害のフォールバック方式が可能少ないエラーが発生しやすいような単純なプロセスを維持するために失敗しないようにしてくださいする必要があります。

障害フォールバック方式が失敗した場合、ユーザーがフォールバック方法を提供することに失敗していない場合、または、Hystrixは、コマンドを実行呼び出し方法に応じて異なる動作を生成します。

  • execute()- 例外をスローします

  • queue()-成功のリターンFutureオブジェクトが、get()メソッドが呼び出されると、例外がスローされます

  • observe()-戻りObservableオブジェクトは、あなたがそれに加入する際に、すぐに加入者の呼び出しますonErrorリクエストアボート方法を

  • toObservable()-戻りObservableオブジェクトは、あなたがそれに加入する際に、すぐに加入者の呼び出しますonErrorリクエストアボート方法を

通常の応答に1.10の復帰

コマンドが正常に実行された場合、Hystrixは、呼び出し元に戻って応答、または貫通しますObservableリターンの形。(セクション2に示すような)実施形態上記コマンド発行によって、Observableオブジェクトは、いくつかの変換になります。

 

2.ヒューズ

ショー下図HystrixCommand又はHystrixObservableCommandどのするHystrixCircuitBreaker相互作用、ならびにHystrixCircuitBreaker内部カウンタを融合させる方法など、決定論理プロセス。

 

  • execute()-発生Future、同じ行動ターゲット.queue()によって生成されFuture、オブジェクトをその呼び出しget()内部によって生成された生成する方法Observable返される応答オブジェクト

  • queue()-内部で生成されたObservableオブジェクトへの変換(デコレータモード)BlockingObservableオブジェクトを、生成して返すためにFuture、オブジェクトを

  • observe()-生産Observableに返されるオブジェクト、あなたが(非同期)実行するコマンドのために(ReplaySubject)に加入し、Observableあなたがそれを呼び出すときに、対象subscribe情報および通知に応答する再生加入の方法は、生成されたユーザに提供する場合

  • toObservable()-戻りObservableオブジェクトは、あなたがその呼び出す必要がありますsubscribeコマンドを実行するための方法を。

クローズドオープンロジックを詳細に次のようになります。

  1. ライン(要求QPS)の容量(介して特定のしきい値に達すると仮定HystrixCommandProperties.circuitBreakerRequestVolumeThreshold()構成)。
  2. 同时,假设线路内的错误率达到一定阈值(通过HystrixCommandProperties.circuitBreakerErrorThresholdPercentage()配置)
  3. 熔断器将从『闭路』转换成『开路』
  4. 若此时是『开路』状态,熔断器将短路后续所有经过该熔断器的请求,这些请求直接走『失败回退逻辑』
  5. 经过一定时间(即『休眠窗口』,通过HystrixCommandProperties.circuitBreakerSleepWindowInMilliseconds()配置),后续第一个请求将会被允许通过熔断器(此时熔断器处于『半开』状态),若该请求失败,熔断器将又进入『开路』状态,且在休眠窗口内保持此状态;若该请求成功,熔断器将进入『闭路』状态,回到逻辑1循环往复。

3. 依赖隔离

Hystrix 通过使用『舱壁模式』(注:将船的底部划分成一个个的舱室,这样一个舱室进水不会导致整艘船沉没。将系统所有依赖服务隔离起来,一个依赖延迟升高或者失败,不会导致整个系统失败)来隔离依赖服务,并限制访问这些依赖服务的并发度。

4. 线程&线程池

通过将对依赖服务的访问执行放到单独的线程,将其与调用线程(例如 Tomcat 线程池中的线程)隔离开来,调用线程能空出来去做其他的工作而不至于被依赖服务的访问阻塞过长时间。

Hystrix 使用独立的,每个依赖服务对应一个线程池的方式,来隔离这些依赖服务,这样,某个依赖服务的高延迟只会拖慢这个依赖服务对应的线程池。

当然,也可以不使用线程池来使你的系统免受依赖服务失效的影响,这需要你小心的设置网络连接/读取超时时间和重试配置,并保证这些配置能正确正常的运作,以使这些依赖服务在失效时,能快速返回错误。

Netflix 在设计 Hystrix 时,使用线程/线程池来实现隔离,原因如下:

  • 多数系统同时运行了(有时甚至多达数百个)不同的后端服务,这些服务由不同开发组开发。

  • 每个服务都提供了自己的客户端库

  • 客户端库经常会发生变动

  • 客户端库可能会改变逻辑,加入新的网络请求

  • 客户端库可能会包含重试逻辑,数据解析,缓存(本地缓存或分布式缓存),或者其他类似逻辑

  • 客户端库对于使用者来说,相当于『黑盒』,其实现细节,网络访问方式,默认配置等等均对使用者透明

  • In several real-world production outages the determination was “oh, something changed and properties should be adjusted” or “the client library changed its behavior.”

  • 即使客户端库本身未发生变化,服务自身发生变化,也可能会影响其性能,从而导致客户端配置不再可靠

  • 中间依赖服务可能包含一些其依赖服务提供的客户端库,而这些库可能不受控且配置不合理

  • 绝大多数网络访问都采用同步的方式进行

  • 客户端代码可能也会有失效或者高延迟,而不仅仅是在网络访问时

4.1 线程池的优势

将依赖服务请求通过使用不同的线程池隔离,其优势如下:

  • 系统完全与依赖服务请求隔离开来,即使依赖服务对应线程池耗尽,也不会影响系统其它请求

  • 降低了系统接入新的依赖服务的风险,若新的依赖服务存在问题,也不会影响系统其它请求

  • 当依赖服务失效后又恢复正常,其对应的线程池会被清理干净,相对于整个 Tomcat 容器的线程池被占满需要耗费更长时间以恢复可用来说,此时系统可以快速恢复

  • 若依赖服务的配置有问题,线程池能迅速反映出来(通过失败次数的增加,高延迟,超时,拒绝访问等等),同时,你可以在不影响系统现有功能的情况下,处理这些问题(通常通过热配置等方式)

  • 若依赖服务的实现发生变更,性能有了很大的变化(这种情况时常发生),需要进行配置调整(例如增加/减小超时阈值,调整重试策略等)时,也可以从线程池的监控信息上迅速反映出来(失败次数增加,高延迟,超时,拒绝访问等等),同时,你可以在不影响其他依赖服务,系统请求和用户的情况下,处理这些问题

  • 线程池处理能起到隔离的作用以外,还能通过这种内置的并发特性,在客户端库同步网络IO上,建立一个异步的 Facade(类似 Netflix API 建立在 Hystrix 命令上的 Reactive、全异步化的那一套 Java API)

简而言之,通过线程池提供的依赖服务隔离,可以使得我们能在不停止服务的情况下,更加优雅地应对客户端库和子系统性能上的变化。

注:尽管线程池能提供隔离性,但你仍然需要对你的依赖服务客户端代码增加超时逻辑,并且/或者处理线程中断异常,以使这些代码不会无故地阻塞或者拖慢 Hystrix 线程池。

4.2 线程池的弊端

使用线程池的主要弊端是会增加系统 CPU 的负载,每个命令的执行,都包含了 CPU 任务的排队,调度,上下文切换。

Netflix 在设计 Hystrix 时,认为相对于其带来的好处,其带来的负载的一点点升高对系统的影响是微乎其微的。

4.3 线程池的开销

Hystrix 的开发人员测试了在子线程中执行construct()run()方法带来的额外时延,以及在父线程中整个请求的耗时,通过这个测试,你能直观了解 Hystrix 使用线程池带来的一点点系统负载的升高影响(线程,监控,日志,熔断器等)。

Netflix API 使用线程池来隔离依赖服务,每天可以处理超过 100 亿的 Hystrix 命令,每个 API 实例有超过 40 个线程池,每个线程池有 5 到 20 个工作线程(绝大部分设置为 10 个线程)。

对于那些本来延迟就比较小的请求(例如访问本地缓存成功率很高的请求)来说,线程池带来的开销是非常高的,这时,你可以考虑采用其他方法,例如非阻塞信号量(不支持超时),来实现依赖服务的隔离,使用信号量的开销很小。但绝大多数情况下,Netflix 更偏向于使用线程池来隔离依赖服务,因为其带来的额外开销可以接受,并且能支持包括超时在内的所有功能。

5. 信号量

除了线程池,队列之外,你可以使用信号量(或者叫计数器)来限制单个依赖服务的并发度。Hystrix 可以利用信号量,而不是线程池,来控制系统负载,但信号量不允许我们设置超时和异步化,如果你对客户端库有足够的信任(延迟不会过高),并且你只需要控制系统负载,那么你可以使用信号量。

HystrixCommandHystrixObservableCommand在两个地方支持使用信号量:

  • 失败回退逻辑:当 Hystrix 需要执行失败回退逻辑时,其在调用线程(Tomcat 线程)中使用信号量

  • 执行命令时:如果设置了 Hystrix 命令的execution.isolation.strategy属性为SEMAPHORE,则 Hystrix 会使用信号量而不是线程池来控制调用线程调用依赖服务的并发度

你可以通过动态配置(即热部署)来决定信号量的大小,以控制并发线程的数量,信号量大小的估计和使用线程池进行并发度估计一样(仅访问内存数据的请求,一般能达到耗时在 1ms 以内,且能达到 5000rps,这样的请求对应的信号量可以设置为 1 或者 2。默认值为 10)。

注意:

  • 如果依赖服务使用信号量来进行隔离,当依赖服务出现高延迟,其调用线程也会被阻塞,直到依赖服务的网络请求超时。
  • 信号量在达到上限时,会拒绝后续请求的访问,同时,设置信号量的线程也无法异步化(即像线程池那样,实现『提交-做其他工作-得到结果』模式)

6. 请求合并

你可以在HystrixCommand之前放置一个『请求合并器』(HystrixCollapser为请求合并器的抽象父类),该合并器可以将多个发往同一个后端依赖服务的请求合并成一个。

下图展示了在两种场景(未增加『请求合并器』和增加『请求合并器』)下,线程和网络连接数量(假设所有请求在一个很小的时间窗口内,例如 10ms,是『并发』的):

6.1 请求合并的优点

在并发执行HystrixCommand时,利用请求合并能减少线程和网络连接数量。通过使用HystrixCollapser,Hystrix 能自动完成请求的合并,开发者不需要对现有代码做批量化的开发。

6.2 全局上下文(适用于所有 Tomcat 线程)

理想情况下,合并过程应该发生在系统全局层面,这样用户发起的,由 Tomcat 线程执行的所有请求都能被执行合并操作。

例如,有这样一个需求,用户需要获取电影评级,而这些数据需要系统请求依赖服务来获取,对依赖服务的请求使用HystrixCommand进行包装,并增加了请求合并的配置,这样,当同一个 JVM 中其他线程需要执行同样的请求时,Hystrix 会将这个请求同其他同样的请求合并,只产生一个网络请求。

注意:合并器会传递一个HystrixRequestContext对象到合并的网络请求中,因此,下游系统需要支持批量化,以使请求合并发挥其高效的特点。

6.3 用户请求上下文(适用于单个 Tomcat 线程)

如果给HystrixCommand只配置成针对单个用户进行请求合并,则 Hystrix 只会在单个 Tomcat 线程(即请求)中进行请求合并。

例如,如果用户想加载 300 个视频对象的书签,请求合并后,Hystrix 会将原本需要发起的 300 个网络请求合并到一个。

6.4 对象模型和代码复杂度

很多时候,当你创建一个对象模型,适用于对象的消费者逻辑,结果发现这个模型会导致生产者无法充分利用其拥有的资源。

例如,这里有一个包含 300 个视频对象的列表,需要遍历这个列表,并对每一个对象调用getSomeAttribute()方法,这是一个显而易见的对象模型,但如果简单处理的话,可能会导致 300 次的网络请求(假设getSomeAttribute()方法内需要发出网络请求),每一个网络请求可能都会花上几毫秒(显然,这种方式非常容易拖慢系统)。

当然,你也可以要求用户在调用getSomeAttribute()之前,先判断一下哪些视频对象真正需要请求其属性。

或者,你可以将对象模型进行拆分,从一个地方获取视频列表,然后从另一个地方获取视频的属性。

但这些实现会导致 API 非常丑陋,且实现的对象模型无法完全满足用户使用模式。 并且在企业级开发时,很容易因为开发者的疏忽导致错误或者不够高效,因为不同的开发者可能有不同的请求方式,这样一个地方的优化不足以保证在所有地方都会有优化。

通过将合并逻辑下沉到 Hystrix 层,不管你如何设计对象模型,或者以何种方式去调用依赖服务,又或者开发者是否意识到这些逻辑需要不需要进行优化,这些都不需要考虑,因为 Hystrix 能统一处理。

getSomeAttribute()方法能放在它最适合的位置,并且能以最适合的方式被调用,Hystrix 的请求合并器会自动将请求合并到合并时间窗口内。

6.5 请求合并带来的额外开销

请求合并会导致依赖服务的请求延迟增高(该延迟为等待请求的延迟),延迟的最大值为合并时间窗口大小。

若某个请求耗时的中位数是 5ms,合并时间窗口为 10ms,那么在最坏情况下(注:合并时间窗口开启时发起请求),请求需要消耗 15ms 才能完成。通常情况下,请求不太可能恰好在合并时间窗口开启时发起,因此,请求合并带来的额外开销应该是合并时间窗口的一般,在此例中是 5ms。

请求合并带来的额外开销是否值得,取决于将要执行的命令,高延迟的命令相比较而言不会有太大的影响。同时,缓存 Key 的选择也决定了在一个合并时间窗口内能『并发』执行的命令数量:如果一个合并时间窗口内只有 1~2 个请求,将请求合并显然不是明智的选择。事实上,如果单线程循环调用同一个依赖服务的情况下,如果将请求合并,会导致这个循环成为系统性能的瓶颈,因为每一个请求都需要等待 10ms 的合并时间周期。

然而,如果一个命令具有高并发度,并且能批量处理多个,甚至上百个的话,请求合并带来的性能开销会因为吞吐量的极大提升而基本可以忽略,因为 Hystrix 会减少这些请求所需的线程和网络连接数量。

6.6 请求合并器的执行流程

7. 请求缓存

HystrixCommandHystrixObservableCommand的实现中,你可以定义一个缓存的 Key,这个 Key 用于在同一个请求上下文(全局或者用户级)中标识缓存的请求结果,当然,该缓存是线程安全的。

下例展示了在一个完整 HTTP 请求周期内,两个线程执行命令的流程:

请求缓存有如下好处:

  • 不同请求路径上针对同一个依赖服务进行的重复请求(有同一个缓存 Key),不会真实请求多次

这个特性在企业级系统中非常有用,在这些系统中,开发者往往开发的只是系统功能的一部分。(注:这样,开发者彼此隔离,不太可能使用同样的方法或者策略去请求同一个依赖服务提供的资源)

例如,请求一个用户的Account的逻辑如下所示,这个逻辑往往在系统不同地方被用到:

Account account = new UserGetAccount(accountId).execute();
//or
Observable<Account> accountObservable = new UserGetAccount(accountId).observe();

Hystrix 的RequestCache只会在内部执行run()方法一次,上面两个线程在执行HystrixCommand命令时,会得到相同的结果,即使这两个命令是两个不同的实例。

  • 数据获取具有一致性

因为缓存的存在,除了第一次请求需要真正访问依赖服务以外,后续请求全部从缓存中获取,可以保证在同一个用户请求内,不会出现依赖服务返回不同的回应的情况。

  • 避免不必要的线程执行

construct()run()方法执行之前,会先从请求缓存中获取数据,因此,Hystrix 能利用这个特性避免不必要的线程执行,减小系统开销。

若 Hystrix 没有实现请求缓存,那么HystrixCommandHystrixObservableCommand的实现者需要自己在construct()run()方法中实现缓存,这种方式无法避免不必要的线程执行开销。

 

 

 

 

 

发布了8 篇原创文章 · 获赞 0 · 访问量 7271

おすすめ

転載: blog.csdn.net/fedorafrog/article/details/104159137