Sentinel-Go 源码系列(二)|初始化流程和责任链设计模式

上节中我们知道了 Sentinel-Go 大概能做什么事情,最简单的例子如何跑起来

其实我早就写好了本系列的第二篇,但迟迟没有发布,感觉光初始化流程显得有些单一,于是又补充了责任链模式,二合一,内容显得丰富一些。

初始化流程

初始化做了什么

Sentinel-Go 初始化时主要做了以下2件事情:

  • 通过各种方式(文件、环境变量等)载入全局配置
  • 启动异步的定时任务或服务,如机器 cpu、内存信息收集、metric log 写入等等

初始化流程详解

提供的 API

上节例子中,我们使用了最简单的初始化方式

1func InitDefault() error

除此之外,它还提供了另外几种初始化方式

1// 使用给定的 parser 方法解析配置的方式来初始化 2func InitWithParser(configBytes []byte, parser func([]byte) (*config.Entity, error)) (err error) 3 4// 使用已解析好的配置对象初始化 5func InitWithConfig(confEntity *config.Entity) (err error) 6 7// 从 yaml 文件加载配置初始化 8func InitWithConfigFile(configPath string) error

从命名能看出它们只是配置的获取方式不一样,其中InitWithParser 有点意思,传入的 parser 是个函数指针,对于 Java 写惯了的我来说还是有点陌生,比如通过 json 解析可以写出如下 parser

1parser := func(configBytes []byte) (*config.Entity, error) { 2 conf := &config.Entity{} 3 err := json.Unmarshal(configBytes, conf) 4 return conf, err 5} 6conf := "{\"Version\":\"v1\",\"Sentinel\":{\"App\":{\"Name\":\"roshi-app\",\"Type\":0}}}" 7err := api.InitWithParser([]byte(conf), parser)

配置项

简单看一下 Sentinel-Go 的配置项,首先配置被包装在一个 Entity 中,包含了一个 Version 和 真正的配置信息 SentinelConfig

1type Entity struct { 2 Version string 3 Sentinel SentinelConfig 4}

接着, SentinelConfig 是这样:

1type SentinelConfig struct { 2 App struct { 3 // 应用名 4 Name string 5 // 应用类型:普通应用,网关 6 Type int32 7 } 8 // Exporter 配置 9 Exporter ExporterConfig 10 // 日志配置 11 Log LogConfig 12 // 统计配置 13 Stat StatConfig 14 // 是否缓存时间戳 15 UseCacheTime bool `yaml:"useCacheTime"` 16}
  • App 应用信息
    • 应用名
    • 应用类型:如普通应用、网关应用等
  • ExporterConfig:prometheus exporter 暴露服务的端口和 path
1type ExporterConfig struct { 2 Metric MetricExporterConfig 3} 4 5type MetricExporterConfig struct { 6 // http 服务地址,如 ":8080" 7 HttpAddr string `yaml:"http_addr"` 8 // http 服务 path,如"/metrics". 9 HttpPath string `yaml:"http_path"` 10}
  • LogConfig:包括使用什么logger,日志目录,文件是否使用 pid(防止一台机器部署两个应用日志混合),以及 metric log 的单个文件大小、最多保留文件个数、刷新时间
1type LogConfig struct { 2 // logger,可自定义 3 Logger logging.Logger 4 // 日志目录 5 Dir string 6 // 是否在日志文件后加 PID 7 UsePid bool `yaml:"usePid"` 8 // metric 日志配置 9 Metric MetricLogConfig 10} 11 12type MetricLogConfig struct { 13 // 单个文件最大占用空间 14 SingleFileMaxSize uint64 `yaml:"singleFileMaxSize"` 15 // 最多文件个数 16 MaxFileCount uint32 `yaml:"maxFileCount"` 17 // 刷新间隔 18 FlushIntervalSec uint32 `yaml:"flushIntervalSec"` 19}
  • StatConfig:统计配置包括资源采集窗口配置,metric 统计的窗口、系统信息收集间隔
1type StatConfig struct { 2 // 全局统计资源的窗口(后续文章再解释) 3 GlobalStatisticSampleCountTotal uint32 `yaml:"globalStatisticSampleCountTotal"` 4 GlobalStatisticIntervalMsTotal uint32 `yaml:"globalStatisticIntervalMsTotal"` 5 // metric 统计的窗口(后续文章再解释) 6 MetricStatisticSampleCount uint32 `yaml:"metricStatisticSampleCount"` 7 MetricStatisticIntervalMs uint32 `yaml:"metricStatisticIntervalMs"` 8 // 系统采集配置 9 System SystemStatConfig `yaml:"system"` 10} 11 12type SystemStatConfig struct { 13 // 采集默认间隔 14 CollectIntervalMs uint32 `yaml:"collectIntervalMs"` 15 // 采集 cpu load 间隔 16 CollectLoadIntervalMs uint32 `yaml:"collectLoadIntervalMs"` 17 // 采集 cpu 使用间隔 18 CollectCpuIntervalMs uint32 `yaml:"collectCpuIntervalMs"` 19 // 采集内存间隔使用 20 CollectMemoryIntervalMs uint32 `yaml:"collectMemoryIntervalMs"` 21}

配置覆盖

从上文知道,参数可以通过自定义 parser / 文件 / 默认 的方式来传入配置,但后面这个配置还可以用系统的环境变量覆盖,覆盖项目前只包括应用名、应用类型、日志文件使用使用 PID 结尾、日志目录

1func OverrideConfigFromEnvAndInitLog() error { 2 // 系统环境变量可覆盖传入的配置 3 err := overrideItemsFromSystemEnv() 4 if err != nil { 5 return err 6 } 7 ... 8 return nil 9}

启动后台服务

  • 启动 聚合 metric 定时任务,聚合后发送到 chan,聚合后的格式如下:
1_, err := fmt.Fprintf(&b, "%d|%s|%s|%d|%d|%d|%d|%d|%d|%d|%d", 2 m.Timestamp, timeStr, finalName, m.PassQps, 3 m.BlockQps, m.CompleteQps, m.ErrorQps, m.AvgRt, 4 m.OccupiedPassQps, m.Concurrency, m.Classification)

时间戳|时间字符串|名称|通过QPS|阻断QPS|完成QPS|出错QPS|平均RT|已经通过QPS|并发|类别

  • 启动 metric 写入日志定时任务,可配置间隔时间(秒级),接受上个任务写入 chan 的数据

  • 启动单独 goroutine 收集 cpu 使用率 / load、内存使用,收集间隔可配置,收集到的信息存放在 system_metric 下的私有变量

1var ( 2 currentLoad atomic.Value 3 currentCpuUsage atomic.Value 4 currentMemoryUsage atomic.Value 5)
  • 若开启,则启动单独 goroutine 缓存时间戳,间隔是 1ms,这个主要是为了高并发下提高获取时间戳的性能
1func (t *RealClock) CurrentTimeMillis() uint64 { 2 // 从缓存获取时间戳 3 tickerNow := CurrentTimeMillsWithTicker() 4 if tickerNow > uint64(0) { 5 return tickerNow 6 } 7 return uint64(time.Now().UnixNano()) / UnixTimeUnitOffset 8}

获取时,如果拿到 0 则说明未开启缓存时间戳,取当前,如果拿到值说明已开启,可直接使用

  • 若配置了 metric exporter,则启动服务,监听端口,暴露 prometheus 的 exporter

责任链模式

什么是责任链模式

可以用这样一张图形象地解释什么是责任链:

责任链模式为每次请求创建了一个,链上有 N 多个处理者,处理者可在不同阶段处理不同的事情,就像这幅图上的小人,拿到一桶水(请求)后都可以完成各自的事情,比如往头上浇,然后再传递给下一个。

为什么叫责任?因为每个处理者只关心自己的责任,跟自己没关系就递交给链上的下一个处理者。

责任链在哪里有用到?很多开源产品都是用了责任链模式,如 DubboSpring MVC 等等

这么设计有什么好处?

  • 简化编码难度,抽象出处理模型,只需关注关心的点即可
  • 扩展性好,如果需要自定义责任链中的一环或者插拔某一环,非常容易实现

关于扩展性除了大家理解的软件设计中的扩展性外,这里还想提两点,阿里开源的软件其实都有高扩展性这个特性,一是因为是开源,别人使用场景未必和自己一致,留出扩展接口,不符合要求的,用户可以自行实现,二是如果要追溯,阿里开源扩展性 Dubbo 可能算是祖师爷(未考证),Dubbo 作者(梁飞)的博客中说过为什么 Dubbo 要设计这么强的扩展性,他对代码有一定的追求,在他维护时期,代码能保证高质量,但如果项目交给别人,如何才能保持现在的水准呢?于是他设计出一套很强的扩展,后面开发基于这个扩展去做,代码就不会差到哪里去

  • 可动态,可针对每个请求构造不同的责任链

Sentinel-Go 责任链设计

先看责任链的数据结构定义,Sentinel-Go 把处理者叫 Slot(插槽),将 Slot 分为了前置统计、规则校验、统计三组,且每组是有有序的

1type SlotChain struct { 2 // 前置准备(有序) 3 statPres []StatPrepareSlot 4 // 规则校验(有序) 5 ruleChecks []RuleCheckSlot 6 // 统计(有序) 7 stats []StatSlot 8 // 上线文对象池(复用对象) 9 ctxPool *sync.Pool 10}

在调用 Entry 开始进入 Sentinel 逻辑时,如果没有手动构造 SlotChain,则使用默认。

为什么这里要设计成三个 Slot组呢?因为每组 Slot 的行为稍有不同,比如前置准备的 Slot 不需要返回值,规则校验组需要返回值,如果校验当前流量不通过,还需要返回原因、类型等信息,统计 Slot 还会有一些入参,比如请求是否失败等等

1type BaseSlot interface { 2 Order() uint32 3} 4 5type StatPrepareSlot interface { 6 BaseSlot 7 Prepare(ctx *EntryContext) 8} 9 10type RuleCheckSlot interface { 11 BaseSlot 12 Check(ctx *EntryContext) *TokenResult 13} 14 15type StatSlot interface { 16 BaseSlot 17 OnEntryPassed(ctx *EntryContext) 18 OnEntryBlocked(ctx *EntryContext, blockError *BlockError) 19 OnCompleted(ctx *EntryContext) 20}

总结

本文从源码角度分析了 Sentinel-Go 的初始化流程和责任链的设计,总体上来说还是比较简单,接下来的系列文章将会分析 Sentinel-Go 的限流熔断等的核心设计与实现。


搜索关注微信公众号"捉虫大师",后端技术分享,架构设计、性能优化、源码阅读、问题排查、踩坑实践。 qrcode_small

点赞
收藏

评论区

加载中...

相关推荐

MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1

文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s

Oracle 分组与拼接字符串同时使用

SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(

MySQL部分从库上面因为大量的临时表tmp_table造成慢查询

背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_

皕杰报表之UUID

​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为

手写Java HashMap源码

HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22

2020年前端实用代码段,为你的工作保驾护航

有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )