序文
クライアント側としてサービス間を呼び出すと、サービスが使用できなくなるのを防ぐ必要があります。これにより、クライアントサービスがクラッシュし、雪崩が発生する可能性があります。
呼び出し元は、「ヒューズ」メカニズムを実現するために、信頼性の低い各サービスを呼び出す必要があります。
- サーキットブレーカは、マイクロサービスの保証に使用されるだけではありません。マイクロサービスアーキテクチャではない場合でも、回路ブレーカーに接続する必要があります。
- ヒューズメカニズムにより、ファントムアクセスが可能になります。
- ヒューズメカニズムには、ゲートウェイ層でのヒューズとコールロケーションでのヒューズの2つのシナリオがあります。
- ゲートウェイ層が融合されています。これには、サービス間のすべての呼び出しがゲートウェイを通過する必要があります。これは、サードパーティサービスへの呼び出しの融合には適していません。また、ゲートウェイを経由せずにサービス間の融合を直接呼び出すことには適していません。ゲートウェイ層ヒューズは、ゲートウェイ+ http / grpc / rpcアーキテクチャに適しています。ゲートウェイ層のヒューズは、発信者のヒューズのスーパーセットです。
- それは呼び出し元で行うことができ、単純なコード侵入があります。この記事も、発信者のサーキットブレーカーに基づいて実装されています。
[融合]:単位時間あたりの特定の要求の失敗数がしきい値に達すると、このタイプの要求は融合状態になります。ヒューズ状態では、後続の要求は、実際にタイムアウトを待機するように要求するのではなく、直接エラーを返します。融合状態には持続時間があります。
[サーキットブレーカーの役割]:grpcサービスによって呼び出されるクライアントの役割、httpサービスによって呼び出されるクライアントの役割、およびtcpサービスによって呼び出されるクライアントの役割。
分析
融合プロセス:
- redisに基づいて分散ヒューズを実現できます。
- ヒューズは一点で実現できます。
融合操作には、次の4つの属性が必要です。
- 【キー】:どの操作を融合するかです。ルートがダウンしているため、サービス全体を利用できなくすることはできません。
- 【FuseTimes】:ヒューズのしきい値。融合をトリガーするために到達した障害の数。
- 【Last】:融合後、融合サイクルはどのくらい続きますか。
- [Perns]:障害の数がヒューズのしきい値に到達してヒューズに入るまでに何秒かかりますか
例:
ユーザー情報の取得要求の場合、10秒以内に50回の失敗があった場合、ユーザー情報の取得要求は20秒間融合します。
{
"key":"/user/get-user-info/",
"fuseTimes": 50,
"last": 20,
"perns": 10
}
成し遂げる
1.redisに基づいて分散型ヒューズを実現します
package redistool
import (
"fmt"
"github.com/garyburd/redigo/redis"
)
// 利用redis,来实现熔断
// 以下,是以http请求熔断示例:
/*
var fuseScheme = NewFuse(20, 30,10) // 10秒内,有20次失败,则会触发熔断,熔断最短持续30秒
func HTTPUtil(url string, ...) error{
key := url
if !fuseScheme.FuseOk(conn, key) {
return errorx.New
}
...
resp, e:= c.Do(req)
if e!=nil {
fuseScheme.Fail(conn, key)
return
}
if resp.StatusCode() == 404 || resp.StatusCode() ==500 {
fuseScheme.Fail(conn, key)
return
}
}
*/
type Fuse struct {
fuseTimes int // fail times trigger fuse. Fuse times is not strictly consistent,because fuse.FuseOK() might read dirty.
last int // fuse lasting seconds
perns int // fail times reach <fuseTimes> per <perns> seconds will trigger fuse opt.
}
func NewFuse(fuseTimes int, last int, perns int) Fuse {
return Fuse{
fuseTimes: fuseTimes,
last: last,
perns: perns,
}
}
// true, 未熔断,放行
// false, 熔断态,禁止通行
func (f Fuse) FuseOk(conn redis.Conn, key string) bool {
rs, e := redis.String(conn.Do("get", fmt.Sprintf("is_fused:%s", key)))
if e != nil && e == redis.ErrNil {
fmt.Printf("get '%s' 未熔断 \n", fmt.Sprintf("is_fused:%s", key))
return true
}
if rs == "fused" {
fmt.Printf("get '%s' 已熔断 \n", fmt.Sprintf("is_fused:%s", key))
return false
}
return false
}
// 某一次请求失败了,则需要调用Fail()
// 当fail次数达到阈值时,将会使得f.FuseOK(conn ,key) 返回false,调用方借此来熔断操作
func (f Fuse) Fail(conn redis.Conn, key string) {
ok := MaxPerNSecond(conn, key, f.fuseTimes, int64(f.perns))
// 未达到配置的熔断阈值,fail无操作
if ok {
return
}
// 达到了熔断点
fmt.Printf("set '%s' 熔断\n", fmt.Sprintf("is_fused:%s", key))
conn.Do("setex", fmt.Sprintf("is_fused:%s", key), f.last, "fused")
}
欠陥
- 障害の数にはウィンドウ期間があり、実際の障害のしきい値はa〜2a回です。aは融合しきい値を表します。
- サービス間にオーバーヘッドがあります。
融着は強い一貫性を保証する必要がないという事実を考慮すると、上記の欠陥は大きな問題ではありません。
2.一点に基づいてヒューズを実現します
シングルポイントヒューズには3つのアイデアがあります。
- ロックされたマップを使用してヒューズを実装する
- マップロックフリーを使用してヒューズを実現します
- ハッシュ+ロックマップを使用してヒューズを実装する
最初のタイプでは、すべてのヒューズキーがレース状態になり、競争シナリオがあります。
2番目のタイプでは、プログラムをinit期間に実行する必要があり、ヒューズキーにアクセスしてマップに登録する必要があります。融合プロセス中、マップは読み取り専用に保たれますが、valueの値はアトミックパッケージを介して直接変更できます。
3番目のタイプは、ハッシュを介してさまざまなキーの競合シナリオを減らすことです。これには、優れた設計基盤が必要です。
ここで、さまざまなビジネスルートに競争状態があってはならず、チーム開発のメンテナンスの粒度を単純化する必要があることはわかっています。これまでのところ、これは3番目の実装に直接基づいています。
package fuse
import (
"fmt"
"github.com/fwhezfwhez/cmap"
"time"
)
type Fuse struct {
m *cmap.MapV2
fuseTimes int
last int // second
perns int // second
}
func NewFuse(fuseTimes int, last int, perns int, slotNum int) Fuse {
return Fuse{
m: cmap.NewMapV2(nil, slotNum, 30*time.Minute),
fuseTimes: fuseTimes,
last: last,
perns: perns,
}
}
func (f *Fuse) FuseTimes() int {
return f.fuseTimes
}
func (f *Fuse) Last() int {
return f.last
}
func (f *Fuse) Perns() int {
return f.perns
}
// true, 未熔断,放行
// false, 熔断态,禁止通行
func (f *Fuse) FuseOk(key string) bool {
fuseKey := fmt.Sprintf("is_fused:%s", key)
v, exist := f.m.Get(fuseKey)
if !exist {
return true
}
vs, ok := v.(string)
if exist && ok && vs == "fused" {
return false
}
return false
}
// 某一次请求失败了,则需要调用Fail()
// 当fail次数达到阈值时,将会使得f.FuseOK(conn ,key) 返回false,调用方借此来熔断操作
func (f *Fuse) Fail(key string) {
multi := time.Now().Unix() / int64(f.perns)
timeskey := fmt.Sprintf("%s:%d", key, multi)
rs := f.m.IncrByEx(timeskey, 1, f.perns)
var ok bool
ok = rs <= int64(f.fuseTimes)
// 未达到配置的熔断阈值,fail无操作
if ok {
return
}
// 达到了熔断点
fuseKey := fmt.Sprintf("is_fused:%s", key)
f.m.SetEx(fuseKey, "fused", f.last)
}
ビジネスでは、ヒューズにアクセスする方法、ここにhttpの例があります。
ヒューズメカニズムをすべてのhttpAPIに接続します
- アクセスパーティはサービスプロバイダーまたはゲートウェイサービスです。サービスパーティ/ゲートウェイサービス自体がハングアップすると、ヒューズメカニズムも無効になります。
- 融合は、サービス全体ではなく、特定のルートのみを融合します
- アクセスヒューズの場合、すべてのサービスステータスは200である必要があります。(410がコードに記述されている理由は、ほとんどのビジネスマンが307、400、および403を使用することを好むためです。これらの3つのコードは、リダイレクト、パラメーターの異常、および認証エラーです。これらの3つのコードは生成が容易であり、ヒューズインジケーターにはなりません。 )
package middleware
import (
"fmt"
"github.com/fwhezfwhez/fuse"
"github.com/gin-gonic/gin"
)
var fm = fuse.NewFuse(20, 10, 5, 128)
func ResetFm(fuseTimes int, last int, pern int, slotNum int) {
fm = fuse.NewFuse(fuseTimes, last, pern, slotNum)
}
func GinHTTPFuse(c *gin.Context) {
if ok := fm.FuseOk(c.FullPath()); !ok {
c.AbortWithStatusJSON(400, gin.H{
"tip": fmt.Sprintf("http api '%s' has be fused for setting {%d times/%ds} and will lasting for %d second to retry", c.FullPath(), fm.FuseTimes(), fm.Perns(), fm.Last()),
})
return
}
c.Next()
if c.Writer.Status() > 410 {
fm.Fail(c.FullPath())
return
}
}
テストケース:
package middleware
import (
"fmt"
"github.com/gin-gonic/gin"
"io/ioutil"
"net/http"
"sync"
"testing"
"time"
)
func TestGinFuse(t *testing.T) {
go func() {
r := gin.Default()
// 加入熔断保障
r.Use(GinHTTPFuse)
r.GET("/", func(c *gin.Context) {
c.JSON(500, gin.H{
"message": "pretend hung up"})
})
r.Run(":8080")
}()
time.Sleep(3 * time.Second)
wg := sync.WaitGroup{
}
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
time.Sleep(time.Duration(time.Now().UnixNano()%20) * time.Millisecond)
defer wg.Done()
rsp, e := http.Get("http://localhost:8080/")
if e != nil {
panic(e)
}
bdb, e := ioutil.ReadAll(rsp.Body)
if e != nil {
panic(e)
}
fmt.Println(rsp.StatusCode, string(bdb))
}()
}
// after 10s, will recover recv 500
time.Sleep(15 * time.Second)
rsp, e := http.Get("http://localhost:8080/")
if e != nil {
panic(e)
}
bdb, e := ioutil.ReadAll(rsp.Body)
if e != nil {
panic(e)
}
fmt.Println(rsp.StatusCode, string(bdb))
wg.Wait()
}
試験結果:
// 阈值前,会返回错误
...
500 {
"message":"pretend hung up"}
500 {
"message":"pretend hung up"}
...
// 达到阈值后,会直接熔断
...
400 {
"tip":"http api '/' has be fused for setting {20 times/5s} and will lasting for 10 second to retry"}
400 {
"tip":"http api '/' has be fused for setting {20 times/5s} and will lasting for 10 second to retry"}
400 {
"tip":"http api '/' has be fused for setting {20 times/5s} and will lasting for 10 second to retry"}
// 睡眠等到熔断时效失效,再次返回错误。无限循环,直到服务恢复
500 {
"message":"pretend hung up"}
結論
- 生産時にヒューズをカスタマイズする方法は?次の点に注意してください。
- すべての障害にはアラームメカニズムが必要です。アラームしきい値は、融合しきい値よりも小さくする必要があります。(融合する前にアラームを受信できることを確認してください)
- 融合しきい値[fuseTimes] / [pern]は適度に高くすることができ、融合時間[last]は適度に低くすることができます。
- オープンソースのヒューズコンポーネントへのアクセスを検討してみませんか?
- ヒューズの実装は複雑ではありません。
- ヒューズコンポーネントは、中央ゲートウェイを使用して自動関連サービスを呼び出すだけで済みます。つまり、サービスがゲートウェイサービスによってのみ呼び出されることを保証しない限り、外部から呼び出すことはできず、サブによって直接接続することもできません。サービス。そうしないと、融合効果が有効になりません。
- 歴史的な理由から、ゲートウェイを経由しない直接サービス呼び出し関係が多数あります。
- サードパーティのサービスを呼び出す必要がある場合は、特定の要求に対してのみサーキットブレーカーを設定する必要があります。
- 定着メカニズムは分散に適していますか?
- 不適当です。単点ヒューズと分散ヒューズに明らかな違いはありません。回数を共有する必要はありません。分散させても、特定のマシンがBサービスネットワークと通信できないと仮定すると、他のマシンは通信できません。通信できれば、このマシンは溶断の数がいっぱいになると、他の通常のサービスも溶断に従います。
- ヒューズ回復メカニズムは、履歴に蓄積された障害の数をクリーンアップするために一度開くか、クリーンアップせずにキー値が自然に無効になるのを待つかを選択します。
- 自然破壊に適しています([最後]を参照)。まず、無効なキー自体は長くなく、10〜15秒間適切であり、重大な結果は発生しません。次に、融合シーンが発生した場合、すぐに回復できないことがよくあります。この10〜15秒は気にしないでください。
- 融合回数をクリアすることには隠れた危険があります。カードマシンのサーバーは、CPU /メモリが高いため、サービスが良い場合と悪い場合があります。クリアされるたびに、しきい値内のリクエストがまだ失敗する可能性があることを意味します。融合にはファントムがあり、失敗の数はしきい値よりはるかに多い可能性があります。したがって、障害が発生したルートの場合、ヒューズ時間はクリーンアップされません。障害が発生した場合は、他の通常のノードがサービスを提供できるように、ヒューズの可能性を高めるのが最善です。