阅读源码的过程,就像是在像武侠小说里阅读武功秘籍一样,分析高手的一招一式,提炼出精髓,来增强自己的内力。

之前的帖子说了一下微服务的雪崩效应和常见的解决方案,太水,没有上代码怎么叫解决方案。github上有很多开源的库来解决雪崩问题,比较出名的是Netflix的开源库hystrix。集流量控制熔断容错等于一身的java语言的库。今天分析的源码库是 hystrix-go,他是hystrix的的go语言版,应该是说简化版本,用很少的代码量实现了主要功能。很推荐朋友们有时间读一读。

使用简单

hystrix的使用是非常简单的,同步执行,直接调用Do方法。

err := hystrix.Do("my_command", func() error {
// talk to other services
return nil
}, func(err error) error {
// do this when services are down
return nil
})

异步执行Go方法,内部实现是启动了一个gorouting,如果想得到自定义方法的数据,需要你传channel来处理数据,或者输出。返回的error也是一个channel

 output := make(chan bool, 1)
errors := hystrix.Go("my_command", func() error {
// talk to other services
output <- true
return nil
}, nil) select {
case out := <-output:
// success
case err := <-errors:
// failure

大概的执行流程图

其实方法DoGo方法内部都是调用了hystrix.GoC方法,只是Do方法处理了异步的过程

func DoC(ctx context.Context, name string, run runFuncC, fallback fallbackFuncC) error {
done := make(chan struct{}, 1)
r := func(ctx context.Context) error {
err := run(ctx)
if err != nil {
return err
}
done <- struct{}{}
return nil
}
f := func(ctx context.Context, e error) error {
err := fallback(ctx, e)
if err != nil {
return err
}
done <- struct{}{}
return nil
}
var errChan chan error
if fallback == nil {
errChan = GoC(ctx, name, r, nil)
} else {
errChan = GoC(ctx, name, r, f)
} select {
case <-done:
return nil
case err := <-errChan:
return err
}
}

自定义Command配置

在调用Do Go等方法之前我们可以先自定义一些配置

	hystrix.ConfigureCommand("mycommand", hystrix.CommandConfig{
Timeout: int(time.Second * 3),
MaxConcurrentRequests: 100,
SleepWindow: int(time.Second * 5),
RequestVolumeThreshold: 30,
ErrorPercentThreshold: 50,
}) err := hystrix.DoC(context.Background(), "mycommand", func(ctx context.Context) error {
// ...
return nil
}, func(i context.Context, e error) error {
// ...
return e
})

我大要说了一下CommandConfig第个字段的意义:

  • Timeout: 执行command的超时时间。默认时间是1000毫秒
  • MaxConcurrentRequests:command的最大并发量 默认值是10
  • SleepWindow:当熔断器被打开后,SleepWindow的时间就是控制过多久后去尝试服务是否可用了。默认值是5000毫秒
  • RequestVolumeThreshold: 一个统计窗口10秒内请求数量。达到这个请求数量后才去判断是否要开启熔断。默认值是20
  • ErrorPercentThreshold:错误百分比,请求数量大于等于RequestVolumeThreshold并且错误率到达这个百分比后就会启动熔断 默认值是50

当然如果不配置他们,会使用默认值

讲完了怎么用,接下来就是分析源码了。我是从下层到上层的顺序分析代码和执行流程

统计控制器

每一个Command都会有一个默认统计控制器,当然也可以添加多个自定义的控制器。

默认的统计控制器DefaultMetricCollector保存着熔断器的所有状态,调用次数失败次数被拒绝次数等等

type DefaultMetricCollector struct {
mutex *sync.RWMutex numRequests *rolling.Number
errors *rolling.Number successes *rolling.Number
failures *rolling.Number
rejects *rolling.Number
shortCircuits *rolling.Number
timeouts *rolling.Number
contextCanceled *rolling.Number
contextDeadlineExceeded *rolling.Number fallbackSuccesses *rolling.Number
fallbackFailures *rolling.Number
totalDuration *rolling.Timing
runDuration *rolling.Timing
}

最主要的还是要看一下rolling.Numberrolling.Number才是状态最终保存的地方

Number保存了10秒内的Buckets数据信息,每一个Bucket的统计时长为1秒

type Number struct {
Buckets map[int64]*numberBucket
Mutex *sync.RWMutex
} type numberBucket struct {
Value float64
}

字典字段Buckets map[int64]*numberBucket 中的Key保存的是当前时间

可能你会好奇Number是如何保证只保存10秒内的数据的。每一次对熔断器的状态进行修改时,Number都要先得到当前的时间(秒级)的Bucket不存在则创建。

func (r *Number) getCurrentBucket() *numberBucket {
now := time.Now().Unix()
var bucket *numberBucket
var ok bool if bucket, ok = r.Buckets[now]; !ok {
bucket = &numberBucket{}
r.Buckets[now] = bucket
} return bucket
}

修改完后去掉10秒外的数据

func (r *Number) removeOldBuckets() {
now := time.Now().Unix() - 10 for timestamp := range r.Buckets {
// TODO: configurable rolling window
if timestamp <= now {
delete(r.Buckets, timestamp)
}
}
}

比如Increment方法,先得到Bucket再删除旧的数据

func (r *Number) Increment(i float64) {
if i == 0 {
return
} r.Mutex.Lock()
defer r.Mutex.Unlock() b := r.getCurrentBucket()
b.Value += i
r.removeOldBuckets()
}

统计控制器是最基层和最重要的一个实现,上层所有的执行判断都是基于他的数据进行逻辑处理的

上报执行状态信息

断路器-->执行-->上报执行状态信息-->保存到相应的Buckets

每一次断路器逻辑的执行都会上报执行过程中的状态,

// ReportEvent records command metrics for tracking recent error rates and exposing data to the dashboard.
func (circuit *CircuitBreaker) ReportEvent(eventTypes []string, start time.Time, runDuration time.Duration) error {
// ...
circuit.mutex.RLock()
o := circuit.open
circuit.mutex.RUnlock()
if eventTypes[0] == "success" && o {
circuit.setClose()
}
var concurrencyInUse float64
if circuit.executorPool.Max > 0 {
concurrencyInUse = float64(circuit.executorPool.ActiveCount()) / float64(circuit.executorPool.Max)
}
select {
case circuit.metrics.Updates <- &commandExecution{
Types: eventTypes,
Start: start,
RunDuration: runDuration,
ConcurrencyInUse: concurrencyInUse,
}:
default:
return CircuitError{Message: fmt.Sprintf("metrics channel (%v) is at capacity", circuit.Name)}
} return nil
}

circuit.metrics.Updates 这个信道就是处理上报信息的,上报执行状态自信的结构是metricExchange,结构体很简单只有4个字段。要的就是

  • channel字段Updates 他是一个有bufferchannel默认的数量是2000个,所有的状态信息都在他里面
  • metricCollectors字段,就是保存的具体的这个command执行过程中的各种信息
type metricExchange struct {
Name string
Updates chan *commandExecution
Mutex *sync.RWMutex metricCollectors []metricCollector.MetricCollector
} type commandExecution struct {
Types []string `json:"types"`
Start time.Time `json:"start_time"`
RunDuration time.Duration `json:"run_duration"`
ConcurrencyInUse float64 `json:"concurrency_inuse"`
} func newMetricExchange(name string) *metricExchange {
m := &metricExchange{}
m.Name = name m.Updates = make(chan *commandExecution, 2000)
m.Mutex = &sync.RWMutex{}
m.metricCollectors = metricCollector.Registry.InitializeMetricCollectors(name)
m.Reset() go m.Monitor() return m
}

在执行newMetricExchange的时候会启动一个协程 go m.Monitor()去监控Updates的数据,然后上报给metricCollectors 保存执行的信息数据比如前面提到的调用次数失败次数被拒绝次数等等

func (m *metricExchange) Monitor() {
for update := range m.Updates {
// we only grab a read lock to make sure Reset() isn't changing the numbers.
m.Mutex.RLock() totalDuration := time.Since(update.Start)
wg := &sync.WaitGroup{}
for _, collector := range m.metricCollectors {
wg.Add(1)
go m.IncrementMetrics(wg, collector, update, totalDuration)
}
wg.Wait() m.Mutex.RUnlock()
}
}

更新调用的是go m.IncrementMetrics(wg, collector, update, totalDuration),里面判断了他的状态

func (m *metricExchange) IncrementMetrics(wg *sync.WaitGroup, collector metricCollector.MetricCollector, update *commandExecution, totalDuration time.Duration) {
// granular metrics
r := metricCollector.MetricResult{
Attempts: 1,
TotalDuration: totalDuration,
RunDuration: update.RunDuration,
ConcurrencyInUse: update.ConcurrencyInUse,
}
switch update.Types[0] {
case "success":
r.Successes = 1
case "failure":
r.Failures = 1
r.Errors = 1
case "rejected":
r.Rejects = 1
r.Errors = 1
// ...
}
// ...
collector.Update(r)
wg.Done()
}

流量控制

hystrix-go对流量控制的代码是很简单的。用了一个简单的令牌算法,能得到令牌的就可以执行后继的工作,执行完后要返还令牌。得不到令牌就拒绝,拒绝后调用用户设置的callback方法,如果没有设置就不执行。

结构体executorPool就是hystrix-go 流量控制的具体实现。字段Max就是每秒最大的并发值。

type executorPool struct {
Name string
Metrics *poolMetrics
Max int
Tickets chan *struct{}
}

在创建executorPool的时候,会根据Max值来创建令牌。Max值如果没有设置会使用默认值10

func newExecutorPool(name string) *executorPool {
p := &executorPool{}
p.Name = name
p.Metrics = newPoolMetrics(name)
p.Max = getSettings(name).MaxConcurrentRequests p.Tickets = make(chan *struct{}, p.Max)
for i := 0; i < p.Max; i++ {
p.Tickets <- &struct{}{}
} return p
}

流量控制上报状态

注意一下字段 Metrics 他用于统计执行数量,比如:执行的总数量,最大的并发数 具体的代码就不贴上来了。这个数量也可以显露出,供可视化程序直观的表现出来。

令牌使用完后是需要返还的,返回的时候才会做上面所说的统计工作。

func (p *executorPool) Return(ticket *struct{}) {
if ticket == nil {
return
} p.Metrics.Updates <- poolMetricsUpdate{
activeCount: p.ActiveCount(),
}
p.Tickets <- ticket
} func (p *executorPool) ActiveCount() int {
return p.Max - len(p.Tickets)
}

一次Command的执行的流程

上面把 统计控制器流量控制上报执行状态讲完了,主要的实现也就讲的差不多了。最后就是串一次command的执行都经历了啥:

 err := hystrix.Do("my_command", func() error {
// talk to other services
return nil
}, func(err error) error {
// do this when services are down
return nil
})

hystrix在执行一次command的前面也有提到过会调用GoC方法,下面我把代码贴出来来,篇幅问题去掉了一些代码,主要逻辑都在。就是在判断断路器是否已打开得到Ticket得不到就限流,执行我们自己的的方法判断context是否Done或者执行是否超时

当然,每次执行结果都要上报执行状态,最后要返还Ticket

func GoC(ctx context.Context, name string, run runFuncC, fallback fallbackFuncC) chan error {
cmd := &command{
run: run,
fallback: fallback,
start: time.Now(),
errChan: make(chan error, 1),
finished: make(chan bool, 1),
}
//得到断路器,不存在则创建
circuit, _, err := GetCircuit(name)
if err != nil {
cmd.errChan <- err
return cmd.errChan
}
//...
// 返还ticket
returnTicket := func() {
// ...
cmd.circuit.executorPool.Return(cmd.ticket)
}
// 上报执行状态
reportAllEvent := func() {
err := cmd.circuit.ReportEvent(cmd.events, cmd.start, cmd.runDuration)
// ...
}
go func() {
defer func() { cmd.finished <- true }()
// 查看断路器是否已打开
if !cmd.circuit.AllowRequest() {
// ...
returnOnce.Do(func() {
returnTicket()
cmd.errorWithFallback(ctx, ErrCircuitOpen)
reportAllEvent()
})
return
}
// ...
// 获取ticket 如果得不到就限流
select {
case cmd.ticket = <-circuit.executorPool.Tickets:
ticketChecked = true
ticketCond.Signal()
cmd.Unlock()
default:
// ...
returnOnce.Do(func() {
returnTicket()
cmd.errorWithFallback(ctx, ErrMaxConcurrency)
reportAllEvent()
})
return
}
// 执行我们自已的方法,并上报执行信息
returnOnce.Do(func() {
defer reportAllEvent()
cmd.runDuration = time.Since(runStart)
returnTicket()
if runErr != nil {
cmd.errorWithFallback(ctx, runErr)
return
}
cmd.reportEvent("success")
})
}()
// 等待context是否被结束,或执行者超时,并上报
go func() {
timer := time.NewTimer(getSettings(name).Timeout)
defer timer.Stop() select {
case <-cmd.finished:
// returnOnce has been executed in another goroutine
case <-ctx.Done():
// ...
return
case <-timer.C:
// ...
}
}() return cmd.errChan
}

dashboard 可视化hystrix的上报信息

代码中StreamHandler就是把所有断路器的状态以流的方式不断的推送到dashboard. 这部分代码我就不用说了,很简单。

需要在你的服务端加3行代码,启动我们的流服务

	hystrixStreamHandler := hystrix.NewStreamHandler()
hystrixStreamHandler.Start()
go http.ListenAndServe(net.JoinHostPort("", "81"), hystrixStreamHandler)

dashboard我使用的是docker版。

docker run -d -p 8888:9002 --name hystrix-dashboard mlabouardy/hystrix-dashboard:latest

在下面输入你服务的地址,我是

http://192.168.1.67:81/hystrix.stream

如果是集群可以使用Turbine进行监控,有时间大家自己来看吧

雪崩利器 hystrix-go 源码分析的更多相关文章

  1. Spring源码分析——资源访问利器Resource之实现类分析

    今天来分析Spring的资源接口Resource的各个实现类.关于它的接口和抽象类,参见上一篇博文——Spring源码分析——资源访问利器Resource之接口和抽象类分析 一.文件系统资源 File ...

  2. Android逆向之旅---反编译利器Apktool和Jadx源码分析以及错误纠正

    Android逆向之旅---反编译利器Apktool和Jadx源码分析以及错误纠正 http://blog.csdn.net/jiangwei0910410003/article/details/51 ...

  3. hystrix 源码分析以及属性的配置

    一.feign与hystix结合 1.1测试环境搭建 架构如图: 非常简单,就是Order服务通过feign调用product服务的一个获取商品信息的一个接口: package com.yang.xi ...

  4. Spring源码分析——资源访问利器Resource之接口和抽象类分析

    从今天开始,一步步走上源码分析的路.刚开始肯定要从简单着手.我们先从Java发展史上最强大的框架——Spring...旗下的资源抽象接口Resource开始吧. 我看了好多分析Spring源码的,每每 ...

  5. tcprstat源码分析之tcp数据包分析

    tcprstat是percona用来监测mysql响应时间的.不过对于任何运行在TCP协议上的响应时间,都可以用.本文主要做源码分析,如何使用tcprstat请大家查看博文<tcprstat分析 ...

  6. Caching-缓存架构与源码分析

    Caching-缓存架构与源码分析 首先奉献caching的开源地址[微软源码] 1.工程架构 为了提高程序效率,我们经常将一些不频繁修改,但是使用了还很大的数据进行缓存.尤其是互联网产品,缓存可以说 ...

  7. Storm源码分析--Nimbus-data

    nimbus-datastorm-core/backtype/storm/nimbus.clj (defn nimbus-data [conf inimbus] (let [forced-schedu ...

  8. SOFA 源码分析 — 自动故障剔除

    前言 集群中通常一个服务有多个服务提供者.其中部分服务提供者可能由于网络,配置,长时间 fullgc ,线程池满,硬件故障等导致长连接还存活但是程序已经无法正常响应.单机故障剔除功能会将这部分异常的服 ...

  9. SOFA 源码分析 — 负载均衡和一致性 Hash

    前言 SOFA 内置负载均衡,支持 5 种负载均衡算法,随机(默认算法),本地优先,轮询算法,一致性 hash,按权重负载轮询(不推荐,已被标注废弃). 一起看看他们的实现(重点还是一致性 hash) ...

  10. SpringCloud(4)---Ribbon服务调用,源码分析

    SpringCloud(4)---Ribbon 本篇模拟订单服务调用商品服务,同时商品服务采用集群部署. 注册中心服务端口号7001,订单服务端口号9001,商品集群端口号:8001.8002.800 ...

随机推荐

  1. SSH框架搭建详细图文教程

    转载请标明原文地址:http://www.cnblogs.com/zhangyukof/p/6762554.html 一.什么是SSH? SSH是JavaEE中三种框架(Struts+Spring+H ...

  2. create-react-app 超级慢的解决方法

    create-react-app超级慢的解决方法 在操作官方实例Create React App时,需要执行指令: create-react-app my-app 来创建一个新的React应用.由于某 ...

  3. IDEA中使用Maven模板创建Maven WebApp项目并使用Tomact来运行项目

    首先需要正确安装Maven和Tomact,Maven安装和Tomact安装步骤,参见别的文章. 一.创建Maven工作空间 点击Finish按钮后,耐心等待.直到出现BUILD SUCCESS为止. ...

  4. Nginx入门教程-简介、安装、反向代理、负载均衡、动静分离使用实例

    场景 Nginx入门简介和反向代理.负载均衡.动静分离理解 https://blog.csdn.net/BADAO_LIUMANG_QIZHI/article/details/102790862 Ub ...

  5. Android 布局阴影实现

    最近项目要求,ui有很多有关于阴影的设计要求,网上找了些实现方式,但都不是很理想.现在闲下来了,就寻思着自己写个阴影布局耍耍,以备后用.先说道说道我找到的几种阴影实现方式: 系统阴影 Andorid ...

  6. CentOS自行编译升级Git

    上一篇升级Git的方式是通过更改yum的源,然后通过yum来安装,那么对于喜欢折腾的人来说,怎么通过Git的源代码自行安装呢? 我安装的是CentOS-7-x86_64-1908,自带的git是1.8 ...

  7. PCA主成分分析(最大投影方差)

    PCA简介: 从n维数据中提取最能代表这组数据的m个向量,也就是对数据进行降维(n->m),提取特征. 目标: 找到一个向量\(\mu\),使n个点在其上的投影的方差最大(投影后的数据越不集中, ...

  8. Yii2中多表关联查询

    准备条件: 1.首先准备两张表: customer(用户表)(id, name) order(订单表)(id, customer_id, price) customer 表和 order 表之间是一对 ...

  9. Spring Boot中使用Swagger2构建强大的RESTful(最新全,无坑)

    1:说明 网上这类文章 太多, 一搜一大把 ,但是要不是知识太过于老旧,就是配置没有说名清楚,你的项目按照他的配置却不能正常运行: 所以本文的目的: 配置swagger 2  那swagger 1 不 ...

  10. Java 8——重复注解和注解的作用范围的扩大化

    一.重复注解 在某些情况下,希望将相同的注解应用于声明或类型用途.从Java SE 8发行版开始,重复注解使可以执行此操作. 例如,正在编写代码以使用计时器服务,该服务使能够在给定时间或某个计划上运行 ...