Sentinel go source code series initialization process and responsibility chain design pattern

Keywords: Go sentinel

In the last section, we learned about what sentinel go can do, and the simplest example is how to run

In fact, I have written the second part of this series for a long time, but it has not been released yet. I feel that the light initialization process is somewhat single, so I added the responsibility chain mode, two in one, and the content is richer.

Initialization process

What did initialization do

During sentinel go initialization, the following two things are mainly done:

  • The global configuration is loaded in various ways (files, environment variables, etc.)
  • Start asynchronous scheduled tasks or services, such as machine cpu, memory information collection, metric log writing, etc

Detailed explanation of initialization process

API provided

In the example in the previous section, we used the simplest initialization method

func InitDefault() error

In addition, it also provides several other initialization methods

// Initialize by parsing the configuration using the given parser method
func InitWithParser(configBytes []byte, parser func([]byte) (*config.Entity, error)) (err error)

// Initialize with the resolved configuration object
func InitWithConfig(confEntity *config.Entity) (err error)

// Load configuration initialization from yaml file 
func InitWithConfigFile(configPath string) error

From the naming, we can see that they are just different ways to obtain the configuration. InitWithParser is a bit interesting. The incoming parser is a function pointer, which is still a little strange to me who is used to writing in Java. For example, the following parser can be written through json parsing

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

Configuration item

Take a brief look at the sentinel go configuration item. First, the configuration is packaged in an Entity, including a Version and the real configuration information sentinel config

type Entity struct {
	Version string
	Sentinel SentinelConfig
}

Next, SentinelConfig is as follows:

type SentinelConfig struct {
	App struct {
		// Application name
		Name string
		// Application type: general application, gateway
		Type int32
	}
	// Exporter configuration
	Exporter ExporterConfig
	// Log configuration
	Log LogConfig
	// Statistical configuration
	Stat StatConfig
	// Cache timestamp
	UseCacheTime bool `yaml:"useCacheTime"`
}
  • App application information
    • Application name
    • Application type: such as general application, gateway application, etc
  • ExporterConfig: prometheus exporter exposes the port and path of the service
type ExporterConfig struct {
	Metric MetricExporterConfig
}

type MetricExporterConfig struct {
	// http service address, such as ": 8080"
	HttpAddr string `yaml:"http_addr"`
	//  http service path, such as "/ metrics"
	HttpPath string `yaml:"http_path"`
}
  • LogConfig: including what logger to use, log directory, whether to use pid for files (to prevent mixing of two application logs deployed on one machine), as well as the single file size, maximum number of reserved files and refresh time of metric log
type LogConfig struct {
	// logger, customizable
	Logger logging.Logger
	// Log directory
	Dir string
	// Add PID after log file
	UsePid bool `yaml:"usePid"`
	// metric log configuration
	Metric MetricLogConfig
}

type MetricLogConfig struct {
  // Maximum space occupied by a single file
	SingleFileMaxSize uint64 `yaml:"singleFileMaxSize"`
	// Maximum number of files
	MaxFileCount      uint32 `yaml:"maxFileCount"`
	// refresh interval 
	FlushIntervalSec  uint32 `yaml:"flushIntervalSec"`
}
  • StatConfig: statistical configuration includes resource collection window configuration, metric statistical window and system information collection interval
type StatConfig struct {
	// Window of global statistical resources (explained later)
	GlobalStatisticSampleCountTotal uint32 `yaml:"globalStatisticSampleCountTotal"`
	GlobalStatisticIntervalMsTotal  uint32 `yaml:"globalStatisticIntervalMsTotal"`
	// Window of metric Statistics (explained later)
	MetricStatisticSampleCount uint32 `yaml:"metricStatisticSampleCount"`
	MetricStatisticIntervalMs  uint32 `yaml:"metricStatisticIntervalMs"`
	// System acquisition configuration
	System SystemStatConfig `yaml:"system"`
}

type SystemStatConfig struct {
	// Acquisition default interval
	CollectIntervalMs uint32 `yaml:"collectIntervalMs"`
	// Acquisition cpu load interval
	CollectLoadIntervalMs uint32 `yaml:"collectLoadIntervalMs"`
	// Acquisition cpu usage interval
	CollectCpuIntervalMs uint32 `yaml:"collectCpuIntervalMs"`
	// Acquisition memory interval usage
	CollectMemoryIntervalMs uint32 `yaml:"collectMemoryIntervalMs"`
}

Configure override

As we know from the above, the parameters can be passed into the configuration through the custom parser / file / default method, but the latter configuration can also be overwritten with the system environment variables. Before overwriting the project, only the application name, application type, PID ending of log file and log directory are included

func OverrideConfigFromEnvAndInitLog() error {
	// The system environment variable overrides the incoming configuration
	err := overrideItemsFromSystemEnv()
	if err != nil {
		return err
	}
	...
	return nil
}

Start background service

  • Start the aggregation metric timing task and send it to chan after aggregation. The format after aggregation is as follows:
_, err := fmt.Fprintf(&b, "%d|%s|%s|%d|%d|%d|%d|%d|%d|%d|%d",
		m.Timestamp, timeStr, finalName, m.PassQps,
		m.BlockQps, m.CompleteQps, m.ErrorQps, m.AvgRt,
		m.OccupiedPassQps, m.Concurrency, m.Classification)

Timestamp | time string | name | block QPS through QPS | complete QPS | error QPS | average RT | has passed QPS | concurrency | category

  • Start the metric log writing timing task. The interval (in seconds) can be configured to accept the data written to chan by the previous task

  • Start a separate goroutine to collect cpu utilization / load and memory usage. The collection interval can be configured, and the collected information is stored in the system_ Private variables under metric

var (
	currentLoad        atomic.Value
	currentCpuUsage    atomic.Value
	currentMemoryUsage atomic.Value
)
  • If enabled, separate goroutine cache timestamps will be started with an interval of 1ms. This is mainly to improve the performance of obtaining timestamps
func (t *RealClock) CurrentTimeMillis() uint64 {
  // Get timestamp from cache
	tickerNow := CurrentTimeMillsWithTicker()
	if tickerNow > uint64(0) {
		return tickerNow
	}
	return uint64(time.Now().UnixNano()) / UnixTimeUnitOffset
}

When obtaining, if you get 0, it means that the cache timestamp is not enabled. If you get the current value, it means that it is enabled and can be used directly

  • If metric exporter is configured, start the service, listen to the port, and expose the exporter of prometheus

Responsibility chain model

What is the responsibility chain model

What is a chain of responsibility can be explained graphically:

The responsibility chain model creates a chain for each request. There are N processors on the chain. Processors can handle different things at different stages. Just like the villain in this picture, they can complete their own things after getting a bucket of water (request), such as pouring water on their head and then passing it to the next one.

Why is it called responsibility? Because each handler only cares about his own responsibility, it has nothing to do with himself, so he will submit it to the next handler in the chain.

Where is the chain of responsibility useful? Many open source products use the responsibility chain model, such as Dubbo, Spring MVC and so on

What are the benefits of this design?

  • Simplify the coding difficulty, abstract the processing model, and only pay attention to the points of concern
  • Good scalability. If you need to customize a link in the responsibility chain or plug a link, it is very easy to implement

As for extensibility, in addition to the extensibility in software design, I would like to mention two points here. Ali's open source software actually has the feature of high extensibility. First, because it is open source, other people's use scenarios may not be consistent with their own, leaving an extension interface. If it does not meet the requirements, users can implement it by themselves. Second, if it needs to be traced, Alibaba's open source extensibility Dubbo may be regarded as the founder (not verified). The author of Dubbo (Liang Fei) said in his blog why Dubbo wants to design such strong extensibility. He has a certain pursuit for the code. During his maintenance period, the code can ensure high quality, but if the project is handed over to others, how can we maintain the current level? So he designed a set of strong extensions, and later development based on this extension, the code will not be worse

  • It can be dynamic, and different responsibility chains can be constructed for each request

Sentinel go responsibility chain design

Let's first look at the definition of the data structure of the responsibility chain. Sentinel go calls the processor Slot and divides the Slot into three groups: pre statistics, rule verification and statistics, and each group is orderly

type SlotChain struct {
	// Pre preparation (orderly)
	statPres []StatPrepareSlot
	// Rule verification (ordered)
	ruleChecks []RuleCheckSlot
	// Statistics (ordered)
	stats []StatSlot
	// Online document object pool (reuse object)
	ctxPool *sync.Pool
}

When calling Entry to enter Sentinel logic, if SlotChain is not constructed manually, the default value is used.

Why are three Slot groups designed here? Because the behavior of each group of slots is slightly different. For example, the pre prepared slots do not need to return a value, and the rule verification group needs to return a value. If the verification of the current traffic fails, you also need to return information such as reason and type. There are also some input parameters in the statistics of slots, such as whether the request fails or not

type BaseSlot interface {
	Order() uint32
}

type StatPrepareSlot interface {
	BaseSlot
	Prepare(ctx *EntryContext)
}

type RuleCheckSlot interface {
	BaseSlot
	Check(ctx *EntryContext) *TokenResult
}

type StatSlot interface {
	BaseSlot
	OnEntryPassed(ctx *EntryContext)
	OnEntryBlocked(ctx *EntryContext, blockError *BlockError)
	OnCompleted(ctx *EntryContext)
}

summary

This paper analyzes the initialization process and responsibility chain design of sentinel go from the perspective of source code. Generally speaking, it is relatively simple. The next series of articles will analyze the core design and implementation of sentinel go's current limiting fuse.

Search focuses on official account of WeChat public, bug catch, technology sharing, architecture design, performance optimization, source code reading, problem checking, and paging practice.

Posted by PatriotXCountry on Tue, 09 Nov 2021 02:23:12 -0800