Follow iLogtail to learn design patterns

Design patterns are an important summary of experience in software development, and the classic design patterns proposed by the Gang of Four (GoF) are known as the "Bible" in design patterns. However, design patterns are often presented in an abstract and theoretical manner. For beginners or developers without much practical experience, learning design patterns directly is often boring.

There are often some books or articles on the market or on the Internet, trying to introduce design patterns in simple terms with practical application scenarios. However, the examples or application practices listed in these materials are often constructed virtual scenes, lacking the real application of production-level software. The most important thing about software theory is to apply what you have learned, so is there any opportunity to learn real production-level code?

iLogtail, as an observable data collector self-developed by the Alibaba Cloud Log Service (SLS) team, has been open-sourced on Github. Its core positioning is to help developers build a unified data collection layer. In the process of technological evolution for many years, iLogtail has been trying to apply various design patterns. The application of these design patterns has greatly improved the quality and maintainability of the software. In this article, we will combine the iLogtail project to discuss the technical principles of some common design patterns from a practical perspective. I would also like to thank many ByteDance students for some upgrades and optimizations of some iLogtail Golang architectures.

If you ever feel that learning design patterns is boring, come and learn iLogtail! Welcome to participate in any form of community discussion and exchange, I believe you will find that learning design patterns can also be a very interesting thing!

Creational patterns

The role of the creational pattern is to provide a general solution to create objects and hide the details of creating objects. When it comes to creating an object, the most familiar thing is to create an object and then set related properties. However, in many scenarios, we need to provide the application side with a more friendly way to create objects, especially in the scenarios of creating various complex classes.

singleton pattern

Pattern introduction

The singleton mode refers to ensuring that only one instance of a class can be generated during the entire system life cycle to ensure the uniqueness of the class. For some resource management scenarios (such as configuration management), it is often necessary to have a global object, which is conducive to coordinating the overall behavior of the system.

iLogtail practice

In iLogtail, collection configuration management plays an important role in connecting user collection configuration and internal collection tasks. By loading and analyzing user collection configurations, specific collection tasks are established.

As a process-level management mechanism, ConfigManager is very suitable for singleton mode. When iLogtail starts, it will initially load all collection configurations, and supports dynamic loading of changed collection configurations during operation. Through the singleton mode, the problem of state synchronization between multiple instances can be effectively avoided; a unified global interface is also provided, which is convenient for each module to call.

class ConfigManager : public ConfigManagerBase {
public:
    static ConfigManager* GetInstance() {
        static ConfigManager* ptr = new ConfigManager();
        return ptr;
    }

// 构造、析构、拷贝构造、赋值构造等均为私有,防止构造多个对象
private:
    ConfigManager();
    virtual ~ConfigManager(); 
    ConfigManager(const ConfigManager&) = delete;
    ConfigManager& operator=(const ConfigManager&) = delete;
    ConfigManager(ConfigManager&&) = delete;
    ConfigManager& operator=(ConfigManager&&) = delete;
};

The GetInstance() function is the key to the singleton mode, which uses static variables and static functions to ensure that there is only one instance of the ConfigManager class in the application. To prevent instantiating multiple ConfigManager objects via copy or assignment, define the copy constructor and assignment operator as private and mark them for deletion.

At the same time, use the Magic Static feature in the C++11 standard: if variables enter the declaration statement concurrently during initialization, the concurrent thread will block and wait for the initialization to complete, ensuring thread safety in concurrent programs.

If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.

factory pattern

Pattern introduction

Factory pattern provides an optimal way to create objects. When creating an object, the creation logic is not exposed to the client. The client only needs to tell the factory class the object to be created, and the rest of the work is done by the factory class.

iLogtail practice

In order to meet the collection and processing requirements of many observable data types, Log, Metric, and Trace are defined in iLogtail C++ Pipeline, and Pipeline Event is abstracted as the general format of Pipeline data flow. As the basic unit of data flow in Pipeline, Pipeline Event often involves a large number of Event applications. Therefore, the Pipeline Event factory is defined in core/models to provide the creation of Log, Metric, Span and other objects, which is convenient for flexible call of data flow and reduces It not only improves the coupling of business scenarios, but also improves the scalability of new data models.

generator pattern

Pattern introduction

Builder mode , also known as builder mode, can create complex objects step by step, allowing the same creation code to generate objects of different types and forms. The objects built by the generator pattern must be large and complex, and must be assembled according to the established manufacturing process, such as automobile production lines.

The Builder pattern consists of four roles:

  • Product: A complex object that consists of multiple parts, each with its own build method and representation.
  • Builder (Abstract Builder): Responsible for defining the abstract interface for building complex objects, including methods for building each component.
  • ConcreteBuilder (concrete generator): implements the Builder interface, is responsible for implementing the construction methods of each component, and finally combines them into a complete complex object.
  • Director (director): responsible for managing the Builder object, call the method of the Builder object to build complex objects. It does not create complex objects directly, but builds complex objects through Builder objects.

iLogtail practice

The Go Pipeline of iLogtail can be regarded as a complex production line, which is a typical generator mode application scenario. First, the Pipeline Manager (Director) decomposes the Pipeline construction process into multiple plug-in construction steps, and the PipeBuilder completes the creation and initialization of plug-ins in each stage; finally, these plug-ins are combined into a complete Pipeline object (Product).

Through the application of the generator mode, the scalability and maintainability of the iLogtail plug-in mechanism are greatly improved, and it is convenient for users to expand various collection and processing scenarios according to actual needs.

prototype pattern

Pattern introduction

The Prototype pattern allows new objects to be created by copying existing objects, rather than by explicit instantiation.

iLogtail practice

The Prototype pattern is often used in scenarios where a large number of similar objects are created. During iLogtail data processing, using the prototype mode to create multiple similar PipelineEvent objects can effectively improve the efficiency and maintainability of data processing.

Summarize

Creational patterns are generally relatively simple, and their role is to generate instance objects.

  • Singleton mode: Ensure that a class has only one instance, and provide a global point of access to the instance. It is suitable for managing some global shared resources and avoiding competition and conflicts among multiple instances, but attention needs to be paid to implementation issues.
  • Factory pattern: Define an interface for creating objects, but let subclasses decide which class to instantiate. Suitable for the creation of objects with similar properties, more flexible.
  • Builder pattern: The construction process of a complex object is divided into multiple steps to complete. It is suitable for creating some complex objects, which is convenient for code maintenance and expansion.
  • Prototype mode: Use the method of copying objects to reduce some complicated creation processes.

structural pattern

The role of structural patterns is to provide a way to organize objects in order to realize the relationship and interaction between objects.

adapter pattern

Pattern introduction

The adapter pattern converts one type of interface into another desired type of interface, enabling objects with incompatible interfaces to work together.

iLogtail application

The iLogtail process consists of two parts. One is the main binary process written in C++, which provides functions such as control, file collection, C++ accelerated processing, and SLS sending; the other is the plug-in part (libPluginBase.so) written in Golang, which is realized through the plug-in system Expansion of processing capacity and richer upstream and downstream ecological support.

In iLogtail, the main implementation logic of the SLS sending scenario is in C++ Sender.cpp, which provides comprehensive sending reliability enhancement capabilities (exception handling, retry, back pressure, etc.). For SlsFlusher in Go Pipeline, it also needs to send the collected and processed data to SLS. If the same logic is implemented on the Go plug-in side, it will cause code redundancy. Therefore, the implementation principle of Go SlsFlusher is to forward the processed data to the C++ part to complete the final data transmission. However, there must be factors of incompatibility in cross-language scenarios. At this time, libPluginAdaptor.so acts as an adapter layer to realize the connection between the Golang sending interface and the C++ sending interface.

appearance mode

Pattern introduction

The Facade pattern aims to provide a simple interface to a library, framework, or other complex class. The facade class usually shields the complex interaction of some subsystems and provides a simple interface, allowing the client to focus on the functions that are really concerned.

iLogtail application

In the scenario where K8s logs are collected to SLS, iLogtail automatically completes the collection configuration by supporting environment variables ( aliyun_logs_{key} ), including creating SLS-related resources such as Project, Logstore, machine group, and collection configuration. There are many overall operations, and many factors need to be considered, such as configuration details, container filter items, operation sequence, and failure.

For the iLogtail Env collection scenario, only a few core configuration items need to be concerned. Therefore, an appearance class that encapsulates the required functions and hides the code details is implemented, which not only simplifies the current calling relationship, but also minimizes the impact of future back-end API upgrades, because only the implementation of the appearance method in the program needs to be modified That's it.

bridge mode

Pattern introduction

The bridge pattern (Bridge Pattern) can split a large class or a series of closely related classes into two independent hierarchies of abstraction and implementation, so that they can be used separately during development. The concept is relatively obscure, and another way to understand it is: a class has two (or more) dimensions that change independently, and these two (or more) dimensions can be expanded independently by means of combination.

iLogtail application

In iLogtail, when using flusher_http to send to different back-end systems, it is often necessary to support request signing and appending auth header, and the request signing algorithm may vary depending on the back-end platform. In order to achieve better scalability, iLogtail provides the extensions mechanism, which separates the implementation of the flusher_http plug-in from the implementation of the specific sending strategy, thereby realizing the scalability of Authenticator, FlushInterceptor, and RequestInterceptors.

Proxy mode

Pattern introduction

The proxy mode is to use a proxy class to hide the implementation details of the concrete implementation class, and is usually used to add some logic before and after the real implementation. Since it is a proxy, it is necessary to hide the real implementation from the client, and the proxy is responsible for all requests from the client.

iLogtail application

In iLogtail, the core step is to ensure that the data is accurately sent to the backend service. In the scenario of sending data to SLS, the most fundamental thing is to call the SDK to send the packaged data. The whole process seems simple but contains great wisdom. Because backend services are complex and changeable, there are often uncertain factors such as network instability, full backend quota, authentication failure, occasional service unavailability, flow control, process restart, etc. If each data sender processes independently and directly calls the SLS SDK for sending, it will inevitably lead to a large number of repeated codes, resulting in increased code complexity. Therefore, iLogtail introduces the Sender proxy class to enhance the reliability of direct SDK sending. The data sender only needs to call Sender::Instance()->Send to consider that the data sending has been completed, and the remaining complex scene processing is all handed over to the Sender class, which guarantees that the data is successfully sent to the backend system.

Summarize

The proxy mode is used to enhance the method; the adapter mode realizes the interface adaptation similar to "packing the chicken into a duck"; the bridge mode realizes the decoupling of the system through combination; the appearance mode allows the client to not care about the instantiation process, Just call the required method.

In addition, there is a combination mode used to describe data with a hierarchical structure; Flyweight mode is used to improve performance in order to cache objects that have been created in specific scenarios.

behavioral model

Behavior patterns are responsible for efficient communication and responsibility delegation between objects. It focuses on the interaction between various classes, and clearly divides responsibilities, making our code clearer.

Observer pattern

Pattern introduction

The Observer pattern defines a one-to-many dependency between objects, similar to the mechanism of subscription and publication. When the state of an observable changes, all objects that depend on it are notified and the event is handled automatically. Through the observer mode, flexible event processing can be realized, and the relationship between objects is looser, which is convenient for system expansion and maintenance.

iLogtail practice

The file collection scenario can be considered as a typical application scenario of the observer mode. In order to take into account collection efficiency and cross-platform support, iLogtail adopts the coexistence mode of polling (polling) and event (inotify). comprehensiveness of the environment.

The log reading behavior is triggered internally in iLogtail in the form of events. Among them, polling and inotify are two independent modules, which respectively store the Create/Modify/Delete events generated by each in Polling Event Queue and Inotify Event Queue, and finally merge into a unified Event Queue.

  • The polling module consists of two threads, DirFilePolling and ModifyPolling. DirFilePolling is responsible for periodically traversing folders according to user configuration, and adding files that meet the log collection configuration into the modify cache; ModifyPolling is responsible for periodically scanning the status of files in the modify cache, comparing the previous status ( Dev, Inode, Modify Time, Size), if an update is found, a modify event will be generated.
  • inotify belongs to the event monitoring mode. It monitors the corresponding directory and subdirectory according to the user configuration. When the monitoring directory changes, the kernel will generate a corresponding notification event.

Finally, the LogInput module completes the consumption of Event Queue consumption, and hands it over to the Event Handler to handle events such as Create/Modify/Delete, and then performs actual log collection.

chain of responsibility model

Pattern introduction

The Chain of Responsibility pattern allows you to send requests down a chain of handlers. After receiving the request, each processor can process the request or pass it to the next processor in the chain.

A chain of responsibility translates certain behaviors into separate objects called handlers. In a lengthy process, each step can be extracted into a class with only a single method to perform the operation, and the request and its data are passed as parameters to the method.

iLogtail practice

The data processing pipeline in iLogtail is a very classic chain of responsibility model. The current main body of the plug-in system is composed of Input, Processor, Aggregator and Flusher. The Processor is the processing layer, which can filter the input data, such as checking whether a specific field meets the requirements or adding, deleting, and modifying fields. Each configuration can configure multiple Processors at the same time, and they adopt a serial structure, that is, the output of the previous Processor is used as the input of the next Processor, and the output of the last Processor is passed to the Aggregator.

memo mode

Pattern introduction

The memento pattern allows capturing the internal state of an object without exposing the details of the object's implementation, and saving this state outside the object, so that the object can be restored to the original saved state later.

The memo mode mainly has the following components:

  • Originator : It mainly records the internal state at the current moment, and is responsible for defining which states belong to the backup range, and is responsible for creating and restoring memorandum data.
  • Memento class (Memento) : Responsible for storing the internal state of the initiator object, and providing the required internal state to the initiator when needed.
  • Management class (Caretaker) : the management class of the memorandum, save and provide the memo. However, the content of the memorandum cannot be accessed and modified.

iLogtail practice

The most important feature in the log collection scenario is to ensure that logs are not lost. iLogtail uses the Checkpoint mechanism to back up the state of file collection to the local disk in time to ensure data reliability in extreme scenarios. Two typical application scenarios:

  • Collect configuration updates/process upgrades

When the configuration is updated or upgraded, the collection needs to be interrupted and the collection context reinitialized. iLogtail needs to ensure that the logs will not be lost even if the logs are rotated during the configuration update or process upgrade.

Solution: In order to ensure that the log data is not lost during the configuration update/upgrade process, before iLogtail reloads the configuration or the process actively exits, it will save all the current collected status to the local checkpoint file; when the new configuration application/process After startup, the last saved checkpoint will be loaded, and the previous collection state will be restored through the checkpoint.

  • Abnormal situations such as process crash and downtime

When the process crashes or goes down, iLogtail needs to provide a fault-tolerant mechanism, so as not to lose data and minimize repeated collection as much as possible.

Solution: There is no time to record checkpoints before exiting due to process crashes or downtime, so iLogtail will periodically dump the collection progress locally: in addition to restoring the normal log file status, it will also search for the rotated logs to minimize the risk of log loss .

iterator pattern

Pattern introduction

The Iterator pattern provides a way to access individual elements of an object without exposing the internal details of the object.

iLogtail practice

The Golang plugin uses LevelDB to back up some context resources and restore data based on iterator mode.

// Iterator iterates over a DB's key/value pairs in key order.
type Iterator interface {
  CommonIterator
  
  // Key returns the key of the current key/value pair, or nil if done.
  // The caller should not modify the contents of the returned slice, and
  // its contents may change on the next call to any 'seeks method'.
  Key() []byte

  // Value returns the key of the current key/value pair, or nil if done.
  // The caller should not modify the contents of the returned slice, and
  // its contents may change on the next call to any 'seeks method'.
  Value() []byte
}

Summarize

Behavioral patterns focus on the ways and patterns of communication and interaction between objects.

  • Observer mode: Defines a one-to-many dependency relationship. When the state of an object changes, all its dependents will be notified and automatically updated.
  • Chain of Responsibility Mode: Decouple the sender and receiver of a request so that multiple objects have the opportunity to process the request until one of the objects processes it successfully.
  • Memento pattern: Allows saving and restoring the previous state of an object without exposing the details of the object's implementation.
  • Iterator pattern: Provides a uniform way to access individual elements in an aggregate object without exposing its internal structure.

reference:

Talk about 23 design patterns in my work

One article explains the design pattern (C++ version)

How to understand these 6 common design patterns?

25,000 words explain 23 design patterns in detail

C++ common design patterns: https://refactoringguru.cn/desi

Author|Yemo

Click to try cloud products for free now to start the practical journey on the cloud!

Original link

This article is the original content of Alibaba Cloud and may not be reproduced without permission.

The third-year junior high school student wrote the web version of Windows 12 deepin -IDE officially debuted, known as "truly independent research and development " . Simultaneously updated", the underlying NT architecture is based on Electron "Father of Hongmeng" Wang Chenglu: The Hongmeng PC version system will be launched next year, and Wenxin will be open to the whole society . Officially released 3.2.0 Green Language V1.0 Officially released
{{o.name}}
{{m.name}}

Guess you like

Origin my.oschina.net/yunqi/blog/10108259