设计模式是软件开发中的重要经验总结,Gang of Four (GoF) 提出的经典设计模式则被誉为设计模式中的“圣经”。然而设计模式往往是以形象和理论化的形式出现,对于初学者或者没有太多实战经验的开发者来说,间接学习设计模式往往会显得枯燥乏味。
市面上或者网上也常常有一些书籍或者文章,尝试以理论的利用场景深入浅出地介绍设计模式。然而这些材料所列举的样例或利用实际,往往都是一些结构的虚构场景,不足生产级软件的实在利用。而软件实践最重要的是学以致用,那是否有实在生产级代码的学习机会呢?
iLogtail 作为一款阿里云日志服务(SLS)团队自研的可观测数据采集器,目前曾经在 Github 开源,其外围定位是帮忙开发者构建对立的数据采集层。iLogtail 在多年的技术演进过程中,也始终在尝试进行各种设计模式的利用,这些设计模式的利用大大晋升了软件的品质与可维护性。本文咱们将联合 iLogtail 我的项目,从实际角度探讨一些常见设计模式的技术原理。在这里也要感激字节跳动多位同学对 iLogtail Golang 局部架构的一些降级优化。
如果你已经感到学习设计模式枯燥无味,那么来学习 iLogtail 吧!欢送参加任何模式的社区探讨交换,置信你会发现学习设计模式也能够是一件十分乏味的事件!
创立型模式
创立型模式的作用是提供一个通用的解决方案来创建对象,并暗藏创立的细节创建对象。说到创立一个对象,最相熟的就是 New 一个对象,而后设置相干属性。然而,在很多场景下,咱们须要给利用方提供更加敌对的创建对象的形式,尤其在创立各种简单类的场景下。
单例模式
模式简介
单例模式是指在整个零碎生命周期内,保障一个类只能产生一个实例,确保该类的唯一性。对于一些资源管理类的场景(例如配置管理),往往须要领有一个全局对象,这样有利于协调系统整体的行为。
iLogtail 实际
在 iLogtail 中,采集配置管理扮演着连接用户采集配置和外部采集工作的重要角色,通过加载与解析用户采集配置,建设具体的采集工作。
作为一个过程级的管理机制,ConfigManager 非常适合采纳单例模式。iLogtail 启动时会初始加载所有采集配置,并反对运行过程中动静加载变更的采集配置。通过单例模式,能够无效防止多个实例间状态同步的问题;也提供了对立的全局接口,不便各个模块进行调用。
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;
};
GetInstance() 函数是单例模式的要害,该函数内应用了动态变量、动态函数的形式,以确保在应用程序中只有一个 ConfigManager 类的实例。为了避免通过拷贝或赋值实例化多个 ConfigManager 对象,将拷贝构造函数和赋值运算符定义为公有,并将其标记为删除。
同时,利用 C++11 规范中的 Magic Static 个性:若变量在初始化时,并发同时进入申明语句,并发线程将会阻塞期待初始化完结,保障了并发程序中的线程平安。
If control enters the declaration concurrently while the variable is being initialized, the concurrent execution shall wait for completion of the initialization.
工厂模式
模式简介
工厂模式提供了一种创建对象的最佳形式。创建对象时不会对客户端裸露创立逻辑,客户端仅须要通知工厂类要创立的对象,其余工作由工厂类实现。
iLogtail 实际
为了应答泛滥可观测数据类型的采集、解决需要,在 iLogtail C++ Pipeline 中定义了 Log、Metric、Trace,并形象出了 Pipeline Event 作为 Pipeline 数据流的通用格局。Pipeline Event 作为 Pipeline 中的数据流转的根本单元,往往会波及大量的 Event 申请,因而在 core/models 中定义了 Pipeline Event 工厂,提供 Log、Metric、Span 等对象的创立,便于数据流灵便调用,升高了业务场景上的耦合,同时进步了数据模型新增时可扩展性。
生成器模式
模式简介
生成器模式又称建造者模式,该模式可能分步骤创立简单对象,容许应用雷同的创立代码生成不同类型和模式的对象。生成器模式所构建的对象肯定是宏大而简单的,并且肯定是依照既定的制作工序将组件组装起来的,例如汽车生产线等。
生成器模式由四个角色组成:
- Product(产品):简单对象,它由多个部件组成,每个部件都有本人的构建办法和示意。
- Builder(形象生成器):负责定义构建简单对象的形象接口,包含构建每个部件的办法。
- ConcreteBuilder(具体生成器):实现 Builder 接口,负责实现各个部件的构建办法,并最终组合成一个残缺的简单对象。
- Director(指挥者):负责管理 Builder 对象,调用 Builder 对象的办法来构建简单对象。它不间接创立简单对象,而是通过 Builder 对象来构建简单对象。
iLogtail 实际
iLogtail 的 Go Pipeline 能够视作一个简单的生产线,是一种典型的生成器模式利用场景。首先,Pipeline 管理器(Director)将 Pipeline 的构建过程合成为多个插件的构建步骤,并由 PipeBuilder 实现各阶段插件的创立和初始化;最初将这些插件组合成一个残缺的 Pipeline 对象(Product)。
通过生成器模式的利用,大大提高 iLogtail 插件机制的可扩展性和可维护性,不便用户依据理论需要进行扩大各类采集和解决场景。
原型模式
模式简介
原型模式容许通过复制现有对象来创立新的对象,而不是通过显式的实例化来创立。
iLogtail 实际
原型模式通常用于创立大量类似对象的场景。在 iLogtail 数据处理过程中,应用原型模式创立多个类似的 PipelineEvent 对象能够无效进步数据处理的效率和可维护性。
总结
创立型模式总体上比较简单,它们的作用就是为了产生实例对象。
- 单例模式:保障一个类只有一个实例,并提供一个拜访该实例的全局点。实用于治理一些全局的共享资源,防止多个实例之间的竞争和抵触,然而须要留神实现上的问题。
- 工厂模式:定义一个用于创建对象的接口,但让子类决定将哪一个类实例化。实用于具备类似性质的对象的创立,更加灵便。
- 生成器模式:将一个简单对象的构建过程分成多个步骤来实现。实用于创立一些简单的对象,不便代码的保护和扩大。
- 原型模式:利用拷贝对象的办法,缩小一些简单的创立过程。
结构型模式
结构型模式的作用是提供一种组织对象的形式,以便实现对象之间的关系和交互。
适配器模式
模式简介
适配器模式将一种类型的接口转换成心愿的另一类接口,使得本来接口不兼容对象可能一起配合工作。
iLogtail 利用
iLogtail 过程由两局部组成,一是 C++ 编写的主体二进制过程,提供了管控、文件采集、C++ 减速解决、SLS 发送等性能;二是 Golang 编写的插件局部(libPluginBase.so),通过插件零碎实现了解决能力的扩大以及更丰盛的上下游生态反对。
在 iLogtail 中,SLS 发送场景次要的实现逻辑在 C++ Sender.cpp,提供了欠缺的发送可靠性加强能力(异样解决、重试、反压等)。而对于 Go Pipeline 中 SlsFlusher 也须要将采集、解决后的数据发送到 SLS,如果在 Go 插件侧也实现雷同的逻辑,会造成代码的冗余。因而,Go SlsFlusher 的实现原理是将解决后的数据转发到 C++ 局部实现最终数据发送。然而跨语言场景必然存在不适配的因素,此时 libPluginAdaptor.so 充当一个适配器层,实现了 Golang 发送接口与 C++ 发送接口之间的连接。
外观模式
模式简介
外观模式旨在为程序库、框架或其余简单类提供一个简略的接口。外观类通常会屏蔽一些子系统的简单交互,提供一个简略的接口,使得客户端聚焦在真正关怀的性能上。
iLogtail 利用
在 K8s 日志采集到 SLS 场景下,iLogtail 通过反对环境变量(aliyun_logs_{key})的形式主动实现采集配置,包含创立 Project、Logstore、机器组、采集配置等 SLS 相干资源。整体操作较多,须要思考配置详情、容器过滤项、操作程序、失败等泛滥因素。
而对于 iLogtail Env 采集场景来说,仅需关怀少数几个外围的配置项即可。因而,实现了一个封装所需性能并暗藏代码细节的外观类,不仅简化了以后的调用关系;还能将将来后端 API 降级所造成的影响最小化,因为只需批改程序中外观办法的实现即可。
桥接模式
模式简介
桥接模式(Bridge Pattern)可将一个大类或一系列严密相干的类拆分为形象和实现两个独立的层次结构,从而能在开发时别离应用。概念比拟艰涩,换一种了解形式:一个类存在两个(或多个)独立变动的维度,能够通过组合的形式,让这两个(或多个)维度能够独立进行扩大。
iLogtail 利用
在 iLogtail 中,应用 flusher_http 发送到不同后端系统时,往往须要反对申请加签、追加 auth header,申请的加签算法可能因后端平台而异。为了实现更好的可扩展性,iLogtail 提供了 extensions 机制,将 flusher_http 插件的实现与具体的发送策略的实现拆散,进而实现了 Authenticator、FlushInterceptor、RequestInterceptors 的可扩展性。
代理模式
模式简介
代理模式就是应用一个代理类来暗藏具体实现类的实现细节,通常还用于在实在的实现的前后增加一部分逻辑。既然说是代理,那就要对客户端暗藏实在实现,由代理来负责客户端的所有申请。
iLogtail 利用
在 iLogtail 中,最外围的步骤就是保证数据精确地发送到后端服务。在将数据发送到 SLS 场景下,最基本的就是调用 SDK 将打包好的数据发送,整个过程看似简略却蕴含着大智慧。因为后端服务是复杂多变的,往往会存在着这种不确定因素,例如网络不稳固、后端 Quota 满、鉴权失败、偶然服务不可用、流控、过程重启等。如果每个数据发送方独立解决间接调用 SLS SDK 进行发送,必然导致大量反复代码,造成代码复杂度减少。因而,iLogtail 引入了 Sender 代理类,加强了间接 SDK 发送的可靠性。数据发送方仅须要调用 Sender::Instance()->Send 即可认为曾经实现了数据发送,剩下的简单场景解决全都交给 Sender 类实现,由 Sender 类保障将数据胜利发送到后端系统。
总结
代理模式用来做办法的加强;适配器模式实现了相似“把鸡包装成鸭”的接口适配;桥梁模式通过组合,实现零碎的解耦;外观模式能够让客户端不须要关怀实例化过程,只有调用须要的办法即可。
此外,还有组合模式用于形容具备层次结构的数据;享元模式为了在特定的场景中缓存曾经创立的对象,用于进步性能。
行为型模式
行为模式负责对象间的高效沟通和职责委派,它关注的是各个类之间的相互作用,将职责划分分明,使得咱们的代码更加地清晰。
观察者模式
模式简介
观察者模式定义了一种对象间的一对多的依赖关系,相似于订阅和公布的机制。当可察看对象的状态产生扭转时, 所有依赖于它的对象都失去告诉并主动进行事件处理。通过观察者模式能够实现灵便的事件处理,使对象间的关系更加涣散,便于零碎的扩大和保护。
iLogtail 实际
文件采集场景能够认为是观察者模式比拟典型的利用场景。为了兼顾采集效率以及跨平台的反对,iLogtail 采纳了轮询(polling)与事件(inotify)并存的模式,既借助了 inotify 的低提早与低性能耗费的特点,也通过轮询的形式兼顾了运行环境的全面性。
iLogtail 外部以事件的形式触发日志读取行为。其中,polling 和 inotify 作为两个独立模块,别离将各自产生的 Create/Modify/Delete 事件,存入 Polling Event Queue 和 Inotify Event Queue 中,并最终合并成一个对立的 Event Queue。
- 轮询模块由 DirFilePolling 和 ModifyPolling 两个线程组成,DirFilePolling 负责依据用户配置定期遍历文件夹,将合乎日志采集配置的文件退出到 modify cache 中;ModifyPolling 负责定期扫描 modify cache 中文件状态,比照上一次状态(Dev、Inode、Modify Time、Size),若发现更新则生成 modify event。
- inotify 属于事件监听形式,依据用户配置监听对应的目录以及子目录,当监听目录存在变动,内核会产生相应的告诉事件。
最终,LogInput 模块实现对 Event Queue 生产的生产,并交由 Event Handler 解决 Create/Modify/Delete 等事件,进而进行理论的日志采集。
责任链模式
模式简介
责任链模式容许你将申请沿着解决者链进行发送。收到申请后,每个解决者均可对申请进行解决,或将其传递给链上的下个解决者。
责任链会将特定行为转换为被称作解决者的独立对象。在一个简短的流程中,每个步骤都可被抽取为仅有单个办法的类,并执行操作,申请及其数据则会被作为参数传递给该办法。
iLogtail 实际
iLogtail 中的数据处理 Pipeline,是十分经典的责任链模式。插件零碎目前的主体由 Input、Processor、Aggregator 和 Flusher 四局部组成,其中 Processor 作为解决层,能够对输出的数据进行过滤,比方查看特定字段是否符合要求或是对字段进行增删改。每一个配置能够同时配置多个 Processor,它们之间采纳串行构造,即上一个 Processor 的输入作为下一个 Processor 的输出,最初一个 Processor 的输入会传递到 Aggregator。
备忘录模式
模式简介
备忘录模式容许在不裸露对象实现细节的状况下,捕捉一个对象的外部状态,并在该对象之外保留这个状态,便于起初将该对象复原到原先保留的状态。
备忘录模式次要有以下几个组成部分:
- 发动人类(Originator):次要记录以后时刻的外部状态,并且负责定义哪些是属于备份范畴的状态,负责创立和复原备忘录数据。
- 备忘录类(Memento):负责存储发起人对象的外部状态,并且在须要的时候向发起人提供须要的外部状态。
- 治理类(Caretaker):备忘录的治理类,保留和提供备忘录。但不能对备忘录的内容进行拜访与批改。
iLogtail 实际
日志采集场景下最重要的个性是保障日志不丢。iLogtail 通过 Checkpoint 机制,及时将文件采集的状态备份到本地磁盘,保障在极其场景下数据的可靠性。两个比拟典型的利用场景:
- 采集配置更新 / 过程降级
配置更新或进行降级时须要中断采集并从新初始化采集上下文,iLogtail 须要保障在配置更新 / 过程降级时,即便日志产生轮转也不会失落日志。
解决思路:为保障配置更新 / 降级过程中日志数据不失落,在 iLogtail 在配置从新加载前或过程被动退出前,会将以后所有采集的状态保留到本地的 checkpoint 文件中;当新配置利用 / 过程启动后,会加载上一次保留的 checkpoint,并通过 checkpoint 复原之前的采集状态。
- 过程 crash、宕机等异常情况
在过程 crash 或宕机时,iLogtail 须要提供容错机制,不丢数据,尽可能地少反复采集。
解决思路:过程 crash 或宕机没有退出前记录 checkpoint 的机会,因而 iLogtail 还会定期将采集进度 dump 到本地:除了恢复正常日志文件状态外,还会查找轮转后的日志,尽可能升高日志失落危险。
迭代器模式
模式简介
迭代器模式提供一种在不裸露对象的外部细节的前提下,拜访对象中各个元素的办法。
iLogtail 实际
Golang 插件应用 LevelDB 进行一些上下文资源的备份,并基于迭代器模式复原数据。
// 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
}
总结
行为模式次要关注对象之间的通信和交互的形式和模式。
- 观察者模式:定义了一种一对多的依赖关系,当一个对象的状态发生变化时,其所有依赖者都会失去告诉并自动更新。
- 职责链模式:将申请的发送者和接收者解耦,使多个对象都有机会解决该申请,直到其中一个对象解决胜利为止。
- 备忘录模式:容许在不裸露对象实现细节的状况下保留和复原对象之前的状态。
- 迭代器模式:提供一种对立的形式来拜访聚合对象中的各个元素,而不须要裸露其内部结构。
作者|烨陌
点击立刻收费试用云产品 开启云上实际之旅!
原文链接
本文为阿里云原创内容,未经容许不得转载。