文章目录
1. 熔断
服务雪崩效应
- 原因:由于延时或负载过高等导致请求积压,占用大量系统资源,服务器达到性能瓶颈,服务提供者不可用
- 现象:上游服务故障导致下游服务瘫痪,出现连锁故障
- 应对策略
扩容
控制流量
熔断
服务降级
熔断器像是一个保险丝。当我们依赖的服务出现问题时,可以及时容错。
一方面可以减少依赖服务对自身访问的依赖,防止出现雪崩效应;
另一方面降低请求频率以方便上游尽快恢复服务。
熔断与限流的区别
限速器(limiter)可以限制接口自身被调的频率
熔断器可监控所调用的服务的失败、超时情况,当依赖的上游服务失败过高时,熔断器开启,不再调用上游服务,转向降级策略,从而避免雪崩。
- 它们最大的区别在于限流主要在server实现,而熔断主要在client实现
熔断器三种状态:
- 关闭状态:服务正常,并维护一个失败率统计,当失败率达到阀值时,转到开启状态
- 开启状态:服务异常,调用 fallback 函数,一段时间之后,进入半开启状态
- 半开启装态:尝试恢复服务,失败率高于阀值,进入开启状态,低于阀值,进入关闭状态
熔断器golang实现
github.com/afex/hystrix-go,提供了 go 熔断器实现,使用上面也很方便,首先创建一个熔断器
hystrix.ConfigureCommand(
"addservice", // 熔断器名字,可以用服务名称命名,一个名字对应一个熔断器,对应一份熔断策略
hystrix.CommandConfig{
Timeout: 100, // 超时时间 100ms
MaxConcurrentRequests: 2, // 最大并发数,超过并发返回错误
RequestVolumeThreshold: 4, // 请求数量的阀值,用这些数量的请求来计算阀值,熔断器是否打开要满足这个条件;这里的设置表示至少有4个请求才进行ErrorPercentThreshold错误百分比计算
ErrorPercentThreshold: 25, // 错误率阀值,百分比。达到阀值,启动熔断
SleepWindow: 1000, // 熔断尝试恢复时间,单位毫秒
},
)
package main
import (
"errors"
"fmt"
"math/rand"
"time"
"github.com/afex/hystrix-go/hystrix"
)
type Product struct {
ID int
Title string
Price int
}
func GetProduct() (Product, error) {
r := rand.Intn(10)
if r < 6 {
time.Sleep(time.Second * 3)
}
return Product{
ID: 1,
Title: "radial",
Price: 13,
}, nil
}
func RecProduct() (Product, error) {
return Product{
ID: 100,
Title: "radial_hks",
Price: 13000,
}, nil
}
// v1.0
func main_old() {
rand.Seed(time.Now().UnixNano())
for {
p, _ := GetProduct()
fmt.Println(p)
time.Sleep(time.Second * 1)
}
}
// v2.0
func main_() {
rand.Seed(time.Now().UnixNano())
for {
err := hystrix.Do("get_product", func() error {
p, _ := GetProduct()
fmt.Println(p)
return nil
}, nil)
if err != nil {
fmt.Println(err)
}
time.Sleep(time.Second * 1)
}
}
// v2.0
func main_2() {
rand.Seed(time.Now().UnixNano())
// config
confidA := hystrix.CommandConfig{
Timeout: 2000,
}
hystrix.ConfigureCommand("get_product", confidA)
for {
err := hystrix.Do("get_product",
func() error {
p, _ := GetProduct()
fmt.Println(p)
return nil
},
nil)
if err != nil {
fmt.Println(err)
}
time.Sleep(time.Second * 1)
}
}
// v3.0
func main_fallback() {
rand.Seed(time.Now().UnixNano())
// config
confidA := hystrix.CommandConfig{
Timeout: 2000,
}
hystrix.ConfigureCommand("get_product", confidA)
for {
err := hystrix.Do("get_product",
func() error {
p, _ := GetProduct()
fmt.Println(p)
return nil
}, func(e error) error {
p, _ := RecProduct()
fmt.Println(p)
fmt.Println("降级用户")
//return nil
return errors.New("time out")
})
if err != nil {
fmt.Println(err)
}
time.Sleep(time.Second * 1)
}
}
// v4.0
func main() {
rand.Seed(time.Now().UnixNano())
// config
confidA := hystrix.CommandConfig{
Timeout: 2000,
MaxConcurrentRequests: 5,
RequestVolumeThreshold: 3,
ErrorPercentThreshold: 20,
SleepWindow: int(time.Second * 15),
}
hystrix.ConfigureCommand("get_product", confidA)
resChan := make(chan Product, 1)
c, _, _ := hystrix.GetCircuit("get_product")
for {
errs := hystrix.Go("get_product",
func() error {
p, _ := GetProduct()
resChan <- p
return nil
}, func(e error) error {
p, _ := RecProduct()
resChan <- p
//return nil
fmt.Println("降级用户")
return errors.New("time out")
})
//if err != nil {
// fmt.Println(err)
//}
select {
case product := <-resChan:
fmt.Println(product)
case err := <-errs:
fmt.Println(err)
}
fmt.Println(c.IsOpen())
time.Sleep(time.Second * 1)
}
}
2. 链路追踪(Tracing)
快速启动Jaeger
为了快速上手, 官方提供了"All in One"的docker镜像, 启动Jaeger服务只需要一行代码:
docker run -d --name jaeger \
-e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
-p 5775:5775/udp \
-p 6831:6831/udp \
-p 6832:6832/udp \
-p 5778:5778 \
-p 16686:16686 \
-p 14268:14268 \
-p 9411:9411 \
--restart=always \
jaegertracing/all-in-one:1.15
现在用于测试的服务端就完成了, 你可以访问http://{host}:16686来访问JaegerUI, 它像这样:
通过Jaeger上报Go应用数据
func NewJaegerTracer(serviceName string) (opentracing.Tracer, io.Closer) {
sender, _ := jaeger.NewUDPTransport("",0)
tracer, closer:= jaeger.NewTracer(serviceName,
jaeger.NewConstSampler(true),
jaeger.NewRemoteReporter(sender))
return tracer, closer
}
在上图可以看到api层一共花了4.03s, 然后其中调用其他服务: 'service-1’花了2.12s, 而service-1又调用了’service-2’花费了2.12s, 用这样的图示很容易就能排查到系统存在的问题. 在这里我只展示了时间, 如果需要追踪其他信息(如错误信息)也是可以实现的.
通过Grpc中间件使用
在单体程序中, 父子Span通过context关联, 而context是在内存中的, 显而易见这样的方法在垮应用的场景下是行不通的.
package main
import (
"context"
"time"
"github.com/Allenxuxu/microservices/lib/tracer"
"github.com/Allenxuxu/microservices/srv/hello/handler"
example "github.com/Allenxuxu/microservices/srv/hello/proto/example"
"github.com/Allenxuxu/microservices/srv/hello/subscriber"
"github.com/micro/go-micro"
"github.com/micro/go-micro/service/grpc"
"github.com/micro/go-micro/util/log"
ocplugin "github.com/micro/go-plugins/wrapper/trace/opentracing"
opentracing "github.com/opentracing/opentracing-go"
)
func Handler(ctx context.Context, msg *example.Message) error {
log.Log("Function Received message: ", msg.Say)
return nil
}
func main() {
// New Service
t, io, err := tracer.NewTracer("go.micro.srv.hello", "")
if err != nil {
log.Fatal(err)
}
defer io.Close()
opentracing.SetGlobalTracer(t)
service := grpc.NewService(
micro.Name("go.micro.srv.hello"),
micro.WrapHandler(ocplugin.NewHandlerWrapper(t)),
micro.RegisterTTL(time.Second*15),
micro.RegisterInterval(time.Second*10),
// micro.Version("latest"),
)
// Initialise service
service.Init()
// Register Handler
example.RegisterExampleHandler(service.Server(), new(handler.Example))
// // Register Struct as Subscriber
// micro.RegisterSubscriber("go.micro.srv.hello", service.Server(), new(subscriber.Example))
// Register Function as Subscriber
micro.RegisterSubscriber("/test", service.Server(), subscriber.Handler)
// Run service
if err := service.Run(); err != nil {
log.Fatal(err)
}
}
package tracer
import (
"io"
"time"
opentracing "github.com/opentracing/opentracing-go"
jaeger "github.com/uber/jaeger-client-go"
jaegercfg "github.com/uber/jaeger-client-go/config"
)
// NewTracer 创建一个jaeger Tracer
func NewTracer(servicename string, addr string) (opentracing.Tracer, io.Closer, error) {
cfg := jaegercfg.Configuration{
ServiceName: servicename,
Sampler: &jaegercfg.SamplerConfig{
Type: jaeger.SamplerTypeConst,
Param: 1,
},
Reporter: &jaegercfg.ReporterConfig{
LogSpans: true,
BufferFlushInterval: 1 * time.Second,
},
}
sender, err := jaeger.NewUDPTransport(addr, 0)
if err != nil {
return nil, nil, err
}
reporter := jaeger.NewRemoteReporter(sender)
// Initialize tracer with a logger and a metrics factory
tracer, closer, err := cfg.NewTracer(
jaegercfg.Reporter(reporter),
)
return tracer, closer, err
}
有时候Jaeger上有数据, 有时候没有
由于客户端和Jaeger-Agent之间是通过UDP协议传输的, 所以如果测试服务器与Jager-Agent服务之间是外网网络环境, 则可能会导致丢包, 通常包越大越容易丢包.
解决办法是将Agent部署到本机, 不过在开发环境为了方便也可以将客户端配置使用Jaeger-Collector, 这时会使用HTTP协议发送Spans.
组件1 : Instrumentation + OpenTracing API
opentracing.SetGlobalTracer() //设置全局的tracer
opentracing.StartSpan() //开始一个span
opentracing.GlobalTracer().Inject() //把span信息注入到http请求里面
opentracing.GlobalTracer().Extract() //从http请求里面解析出上一个服务的span信息