概述
Aliware
走近iLogtail
iLogtail架构发展历程
单一文件采集阶段
只能采集日志文件;
假定日志为单一格式,每种格式的日志仅支持一种处理方式(如正则解析、Json解析等);
只能将日志发送至日志服务;

完全由C++实现,在日志采集方面具有显著优势;
由于需求单一,因此整体架构偏向于单体架构,代码设计以面向过程为主,类的功能划分不明确,多个模块使用同一个类对象,导致类间依赖严重,可扩展性较差;
功能实现与日志服务相关概念(如LogGroup和Logstore等)强绑定,普适性较差;
Golang插件扩展阶段
多样化的数据输入输出选项
个性化的数据处理能力组合
高性能的数据处理吞吐能力

每个采集配置对应一条完整的流水线,各个流水线之间的资源互相独立,互不影响;
每条流水线支持多个输入和输出,同时支持从C++主程序中接收数据及向C++主程序发送数据;
每条流水线支持多个处理插件级联,有效提升处理能力;
插件系统本身具备配置管理能力,支持配置的热加载,可独立于C++主程序进行工作。
1. 采集日志文件并使用C++的处理能力,最后将数据投递至日志服务SLS(1和4号组合);
2. 采集日志文件并使用Golang插件进行处理,最后将数据投递至日志服务SLS(2和4号组合);
3. 采集日志文件并使用Golang插件进行处理,最后将数据投递至第三方存储(2和5号组合);
4. 采集其它输入(如syslog)并使用Golang插件进行处理,最后将数据投递至日志服务SLS(3和4号组合);
5. 采集其它输入(如syslog)并使用Golang插件进行处理,最后将数据投递至第三方存储(3和5号组合)。
C++和Golang多语言实现,C++部分拥有性能优势,Golang部分拥有功能优势; 支持多样化输入和输出选项; 数据处理能力有一定提升,但输入输出与处理模块间的组合能力存在多种限制: C++部分原生的高性能处理能力仍然局限于采集日志文件并投递至日志服务的场景使用; C++部分的高性能处理能力无法与插件系统的多样化处理能力相结合,二者只能选其一,从而降低了复杂日志处理场景的性能。
为什么要重构?
1. 由于C++主程序代码存在错综复杂的类间依赖关系,导致开发难度极大,加之C++的处理能力无法被社区所使用,因此C++主程序的开发几乎无人问津。
2. 不论是C++主程序还是Golang插件系统,其内部的数据交互模型只适用于可观测数据中的Log,而无法表达Metric和Trace。除此以外,这些数据结构均针对SLS而设计,导致在向第三方存储系统投递数据时,必须进行额外的数据结构转换,从而降低整体的性能。
3. 碍于C++主程序代码错综复杂的类间依赖关系,商业版代码与开源版的剥离只能采用非常原始和丑陋的文件替换方式。这种操作直接导致如下两个结果:
开源版代码中存在大量意义不明的无用空函数;
在进行商业版代码开发时,首先需要进行文件替换,从而容易引入开源版和商业版代码的不一致,对联调联测带来诸多不便,影响开发和发布效率。
综上所述,不论是从产品演进,还是从开发体验,原有iLogtail架构已经严重制约了其快速发展。因此,对iLogtail的架构进行升级已经迫在眉睫。
目标
Aliware
《重构:改善既有代码的设计》一书中对重构的意义和方法论有详细的阐述。对于iLogtail而言,本次重构的主要目标不仅仅停留于工程层面的优化,更重要的是通过对原有架构的升级来支撑产品未来的快速发展。具体来说,本次架构升级可以分为如下几个目标:
1. 将iLogtail的内部数据模型更换为通用数据模型,以减少数据投递时不必要的数据结格式转换;
2. 将C++主程序的输入、处理和输出能力全面插件化,便于从产品侧统一C++部分和Golang部分的插件概念;
3. 在C++主程序中增加可观测流水线的概念,强化C++主程序的流水线配置管理能力,以支持C++处理能力间的级联和C++处理能力与Golang处理能力的组合,从而增强C++的主体地位;
4. 统一商业版和开源版的采集配置格式,均采用流水线模式的配置结构,以适应最新的iLogtail架构;
5. 优化采集配置热加载的方式,提升配置容错能力;
6. 优化商业版代码嵌入开源版代码的路径,通过仅追加文件而非切换文件的方式来实现,提升开发效率。
实践
Aliware
数据模型通用化
message Log{required uint32 Time = 1;message Content{required string Key = 1;required string Value = 2;}repeated Content Contents= 2;repeated string values = 3;optional fixed32 Time_ns = 4;}message LogTag{required string Key = 1;required string Value = 2;}message LogGroup{repeated Log Logs= 1;optional string Category = 2;optional string Topic = 3;optional string Source = 4;optional string MachineUUID = 5;repeated LogTag LogTags = 6;}
可以看到,每个LogGroup包含若干Log以及LogTag,以及其他一些元信息。显然,使用这个数据结构作为iLogtail内部的数据模型是有所不足的:
如LogGroup这个名字所示,该数据结构仅适用于表达可观测数据中的Log,而无法表达Metric和Trace,缺乏普适性;
LogGroup是一个PB,应当只是在最终发送数据时使用,而不适合作为通用的内存数据模型。另一方面,这个PB只适用于SLS,并不适用于其他第三方存储。因此,在往第三方存储发送数据时,需要额外进行数据格式转换,降低采集效率。
支持表达可观测数据的所有类型,包括Log、Metric和Trace,提升数据结构的普适性;
发送模块可根据自身需要,选择不同的协议对通用数据结构进行序列化,提升发送协议的灵活性和性能。

PipelineEventGroup
mEvents:一组事件;
mMetadata:EventGroup共享的元信息,例如机器ip、容器名称、日志路径等;仅在生成EventGroup时可写,且保存于内存中,不用于最终输出;
mTags:EventGroup共享的tag,与原有架构中的LogTag相对应,用于保存mMetadata中用户需要实际输出的信息,仅在tag处理插件中可写;
mSourceBuffer:EventGroup共享的内存分配器,所有成员变量涉及的内存分配均需由该分配器分配。
PipelineEvent
PipelineEventPtr
插件抽象
可观测流水线是现代可观测数据采集器的必要元素,而可观测流水线的核心组成是插件,包括输入、处理和输出插件。在原有架构中,只有Golang插件系统存在插件概念,C++主程序中缺乏相关概念。因此,为了能够建立统一的流水线,必须在C++主程序中新增插件的概念。
class Plugin {public:virtual ~Plugin() = default;virtual const std::string& Name() const = 0;// other setters && gettersprotected:PipelineContext* mContext = nullptr;};
其中,成员变量mContext指向插件所属流水线(Pipeline)的上下文信息(具体含义将在下文介绍),成员函数Name()返回该插件的名字。

处理插件
接口定义
class Processor : public Plugin {public:virtual ~Processor() {}virtual bool Init(const Json::Value& config) = 0;virtual void Process(std::vector<PipelineEventGroup>& logGroupList);protected:virtual bool IsSupportedEvent(const PipelineEventPtr& e) const = 0;virtual void Process(PipelineEventGroup& logGroup) = 0;};
其中,公有成员函数的说明如下:
Init函数:负责根据采集配置实例化插件,并返回是否成功实例化; Process函数:负责对输入的每一个PipelineEventGroup进行处理,并将处理结果通过同一变量返回。
原有能力抽象

显然,将日志文件读取和日志解析的能力统一放到一个类中是一个不太合理的设计,完全缺乏可扩展性。为此,我们需要将日志切分(LogSplit函数)和日志解析(ParseLogLine函数)的能力从LogFileReader类中剥离开来,同时和LogParser类中相关的函数进行重新组合,从而形成多个独立的处理插件。
6. ProcessorParseTimestampNative:时间解析插件,用于解析事件中记录时间的字段,并将结果置为事件的__time__字段;
输入插件
接口定义
class Input : public Plugin {public:virtual ~Input() = default;virtual bool Init(const Json::Value& config, Json::Value& optionalGoPipeline) = 0;virtual bool Start() = 0;virtual bool Stop(bool isPipelineRemoving) = 0;};
其中,公有成员函数的说明如下:
Init函数:负责根据采集配置实例化插件,并返回是否成功实例化以及可能的Golang流水线组件;
Start函数:启动输入插件;
Stop函数:根据流水线是否即将被移除,采取不同的策略停止输入插件;
原有能力抽象

bool InputFile::Start() {if (!FileServer::GetInstance()->IsRunning()) {FileServer::GetInstance()->Start();}if (mEnableContainerDiscovery) {mFileDiscovery.SetContainerInfo(FileServer::GetInstance()->GetAndRemoveContainerInfo(mContext->GetPipeline().Name()));}FileServer::GetInstance()->AddFileDiscoveryConfig(mContext->GetConfigName(), &mFileDiscovery, mContext);FileServer::GetInstance()->AddFileReaderConfig(mContext->GetConfigName(), &mFileReader, mContext);FileServer::GetInstance()->AddMultilineConfig(mContext->GetConfigName(), &mMultiline, mContext);FileServer::GetInstance()->AddExactlyOnceConcurrency(mContext->GetConfigName(), mExactlyOnceConcurrency);return true;}bool InputFile::Stop(bool isPipelineRemoving) {if (!FileServer::GetInstance()->IsPaused()) {FileServer::GetInstance()->Pause();}if (!isPipelineRemoving && mEnableContainerDiscovery) {FileServer::GetInstance()->SaveContainerInfo(mContext->GetPipeline().Name(), mFileDiscovery.GetContainerInfo());}FileServer::GetInstance()->RemoveFileDiscoveryConfig(mContext->GetConfigName());FileServer::GetInstance()->RemoveFileReaderConfig(mContext->GetConfigName());FileServer::GetInstance()->RemoveMultilineConfig(mContext->GetConfigName());FileServer::GetInstance()->RemoveExactlyOnceConcurrency(mContext->GetConfigName());return true;}
可以看到,文件输入插件InputFile类的Start函数只做了如下两件事:
如果文件采集总线程未启动,则调用FileServer类的Start函数启动线程; 将插件相关配置注册到FileServer类中。
如果文件采集线程未暂停,则调用FileServer类的Stop函数暂停全局文件采集; 将插件相关配置从FileServer类中删除。
可扩展性
输出插件
接口定义
class Flusher : public Plugin {public:virtual ~Flusher() = default;virtual bool Init(const Json::Value& config, Json::Value& optionalGoPipeline) = 0;virtual bool Start() = 0;virtual bool Stop(bool isPipelineRemoving) = 0;};
其中,公有成员函数的说明如下:
Init函数:负责根据采集配置实例化插件,并返回是否成功实例化以及可能的Golang流水线组件;
Start函数:启动输出插件;
Stop函数:根据流水线是否即将被移除,采取不同的策略停止输出插件;
原有能力抽象

bool FlusherSLS::Start() {SLSSender::Instance()->IncreaseProjectReferenceCnt(mProject);SLSSender::Instance()->IncreaseRegionReferenceCnt(mRegion);SLSSender::Instance()->IncreaseAliuidReferenceCntForRegion(mRegion, mAliuid);return true;}bool FlusherSLS::Stop(bool isPipelineRemoving) {SLSSender::Instance()->DecreaseProjectReferenceCnt(mProject);SLSSender::Instance()->DecreaseRegionReferenceCnt(mRegion);SLSSender::Instance()->DecreaseAliuidReferenceCntForRegion(mRegion, mAliuid);return true;}
可以看到,SLS输出插件flusherSLS类的Start和Stop函数只是将插件相关配置注册到SLSSender类中或从类中删除。显然,通过这种代理模式,我们也将SLS发送的具体实现隐藏在SLS输出插件背后,从而对外提供了统一的接口描述。
可扩展性
流水线抽象
与插件类似,在原有架构中,仅Golang插件系统存在流水线的概念,C++主程序中仅有采集配置而缺乏流水线的概念。因此,需要在C++主程序中新增流水线概念,这样做的好处在于:
统一C++主程序和Golang插件系统的流水线,加强C++主程序的主体地位;
支持C++处理能力的级联,极大地提升C++部分对于复杂日志的处理能力;
便于C++插件和Golang插件的组合,从而提供更灵活的插件编排能力,同时从产品层面提供更加统一的视图。
插件编排

从产品层面,扩展处理插件仅起到辅助作用,仅在单纯使用原生处理插件无法满足处理需求时使用。因此,扩展处理插件相对于原生处理插件而言是补充和追加关系,而非对等关系。 从架构层面,毕竟原生处理插件和扩展处理插件分别由不同的语言实现,二者之间的交互必须通过CGO接口来完成。从性能角度,应当尽可能避免频繁的CGO接口调用,因此在处理阶段,只允许数据单向地从C++主程序流向Golang插件系统。
根据以上描述,在新架构中,数据的可能通路如下所示:

数据通路应尽可能多地经过C++组件;
数据通路应当尽可能减少CGO接口;

由于Golang插件系统的运行也是以流水线的形式进行的,并不能以插件的形式单独存在,因此我们重新定义Golang流水线为流水线的子流水线。从上图中也能看到,Golang子流水线可能有两种形式:
包含输入插件:如2、3、5组合或者2、4组合;
不包含输入插件:如1、3、4组合。
插件实例

可以看到,每个插件实例都有一个mId成员用于唯一标识一个插件实例,以及一个mPlugin成员指向真正的插件。
反馈队列
LogStore是SLS特有的概念,对于往第三方存储发送数据的场景(例如开源场景),没有Logstore的概念,只能默认所有的配置共用一个Logstore,即所有配置共用一个缓冲队列。这显然是不合理的。
即便是往日志服务投递数据的场景,由于一个Logstore包含多个采集配置,因此难以通过反馈队列实现配置级的资源管控。

与Golang插件系统类似,每个流水线拥有一个独立的处理队列;
对于发送队列,从Process线程的角度看,每个发送插件拥有一个发送队列。但实际上这个发送队列内部可进一步包含多个子队列,例如SLS输出插件仍然保持每个Logstore一个发送队列;
发送队列与处理队列之间的反馈不再是一对一的关系,而是改成多对一的关系。
使用这样的架构有如下好处:
由于流水线的资源管控往往只需要对流水线的源头进行控制即可,因此处理队列保持以流水线为粒度能够保证流水线资源控制的正常进行,同时还便于对流水线进行优先级的区分。
由于不同的发送服务端有着不同的资源管控粒度(例如SLS对Logstore的流量有限制),但这些细节对于Process线程来说没有意义。因此,通过设计模式中的代理模式(Proxy)保持一个发送插件实例一个逻辑上的发送队列能够最大程度简化类间交互,增强可扩展性,同时降低内存使用。
运用设计模式中的观察者模式(Observer)有助于提升反馈队列交互的可扩展性。
流水线定义
class Pipeline {public:bool Init(Config&& config);void Start();void Process(std::vector<PipelineEventGroup>& logGroupList);void Stop(bool isRemoving);// other getters & settersprivate:std::string mName;std::vector<std::unique_ptr<InputInstance>> mInputs;std::vector<std::unique_ptr<ProcessorInstance>> mProcessorLine;std::vector<std::unique_ptr<FlusherInstance>> mFlushers;Json::Value mGoPipelineWithInput;Json::Value mGoPipelineWithoutInput;FeedbackQueue<PipelineEventGroup> mProcessQueue;mutable PipelineContext mContext;std::unique_ptr<Json::Value> mConfig;// other private members};
其中的公有成员函数说明如下:
Init函数:根据采集配置进行插件编排,实例化所有的C++插件,并加载可能存在的Golang子流水线; Start函数:按照从输出到输出的顺序(即数据通路图中的5至1顺序)依次启动各个组件; Process函数:按顺序使用C++插件对输入的PipelineEventGroup列表进行处理; Stop函数:按照从输入到输出的顺序(即数据通路图中的1至5顺序)依次停止各个组件。
mName:流水线的名字,与采集配置名相同;
mInputs:C++输入插件实例列表;
mProcessors:原生处理插件实例列表;
mflushers:C++输出插件实例列表;
mGoPipelineWithInput:包含输入插件的Golang子流水线,可选;
mGoPipelineWithoutInput:不包含输入插件的Golang子流水线,可选;
mContext:流水线上下文;
mProcessQueue:当前流水线的处理队列;
mConfig:采集配置的原始内容。
mConfigName:流水线的名称;
mGlobalConfig:流水线级别的配置,由采集配置给出;
mPipeline:指向当前流水线的指针;
mLogger和mAlarm:用于打印日志和发送告警的全局组件。
采集配置管理优化
原有代码中的采集配置管理模块基本由ConfigManager类来负责,但实现极为混乱,没有任何设计思想可言,和其它模块的耦合严重,在开源社区面临“一改就错”的尴尬境地。因此,在新架构中,原有的采集配置管理模块将全部废弃,在保证兼容性的前提下,重新设计相关功能。
配置格式
商业版配置:采用平铺结构,没有任何层次,且仅支持JSON格式
{"aliuid": "1234567890","category": "test_logstore","create_time": 1693370409,"defaultEndpoint": "cn-shanghai-intranet.log.aliyuncs.com","delay_alarm_bytes": 0,"delay_skip_bytes": 0,"discard_none_utf8": false,"discard_unmatch": true,"docker_exclude_env": {},"docker_exclude_label": {},"docker_file": false,"docker_include_env": {},"docker_include_label": {},"enable": true,"enable_tag": false,"file_encoding": "utf8","file_pattern": "*.log","filter_keys": [],"filter_regs": [],"group_topic": "aaaaaaab","keys": ["k1,k2"],"local_storage": true,"log_begin_reg": ".*","log_path": "/home","log_type": "common_reg_log","log_tz": "","max_depth": 10,"max_send_rate": -1,"merge_type": "topic","preserve": true,"preserve_depth": 1,"priority": 0,"project_name": "test-project","raw_log": false,"regex": ["(\\d+)x(.*)"],"region": "cn-shanghai","send_rate_expire": 0,"sensitive_keys": [],"tail_existed": false,"timeformat": "","topic_format": "none","tz_adjust": false,"version": 3}
开源版配置:采用流水线结构,有较好的层次,但只支持YAML格式
inputs:- Type: file_logLogPath: /homeFilePattern: '*.log'MaxDepth: 10processors:- Type: processor_regex_accelerateRegex: '(\\d+)x(.*)'Keys: ["k1", "k2"]flushers:- Type: flusher_slsProjectName: test-projectLogstoreName: test_logstoreEndpoint: cn-shanghai-intranet.log.aliyuncs.com
为了匹配新架构,iLogtail 2.0启用全新的采集配置结构:

其中,inputs、processors、aggregators和flushers中可包含任意数量的插件,包括C++插件和Golang插件。
配置文件组织
商业版管控端下发的配置为一个文件存放所有的采集配置,文件格式仅支持JSON,默认位置为/usr/local/ilogtail/user_log_config.json; 本地商业版配置既支持一个文件一个采集配置,也支持一个文件多个采集配置,文件格式仅支持JSON,默认存放位置为/etc/ilogtail/user_config.d目录和user_local_config.json; 本地开源版配置仅支持一个文件一个采集配置,文件格式仅支持YAML,默认存放位置为/etc/ilogtail/user_yaml_config.d目录; 开源版管控端下发的配置为一个文件一个采集配置,文件格式仅支持YAML,默认存放位置为/etc/ilogtail/remote_yaml_config.d目录;
每个文件存放一个采集配置,文件名即为采集配置名; 文件名后缀标识文件格式,支持json和yaml(或yml); 同一来源的采集配置放在同一个目录下,默认存放位置为/etc/ilogtail/config/<source>,其中<source>代表来源,目前包括:
商业版管控端下发的配置:enterprise
开源版管控端下发的配置:common
本地:local
配置热加载
class ConfigWatcher {public:static ConfigWatcher* GetInstance();ConfigDiff CheckConfigDiff();void AddSource(const std::string& dir, std::mutex* mux = nullptr);private:std::vector<std::filesystem::path> mSourceDir;std::map<std::string, std::pair<uintmax_t, std::filesystem::file_time_type>> mFileInfoMap;other members};
可以看到,ConfigWatcher类对外提供两个方法:
AddSource函数:向mSourceDir注册新的需要监控的存放采集配置的目录; CheckConfigDiff函数:检查所有被监控目录的采集配置文件是否有改变,返回新增、删除和存在修改的配置(记录在ConfigDiff结构体中),并在mFileInfoMap中更新最新的文件状态。

当CheckConfigDiff函数返回非空,则会进一步调用PipelineManager类的UpdatePipelines函数将配置加载成实际的流水线:
void logtail::PipelineManager::UpdatePipelines(ConfigDiff& diff) {for (const auto& name : diff.mRemoved) {mPipelineNameEntityMap[name]->Stop(true);mPipelineNameEntityMap.erase(name);}for (auto& config : diff.mModified) {auto p = BuildPipeline(std::move(config));if (!p) {continue;}mPipelineNameEntityMap[config.mName]->Stop(false);mPipelineNameEntityMap[config.mName] = p;p->Start();}for (auto& config : diff.mAdded) {auto p = BuildPipeline(std::move(config));if (!p) {continue;}mPipelineNameEntityMap[config.mName] = p;p->Start();}}
远程配置下发
class ConfigProvider {public:virtual void Init(const std::string& dir);virtual void Stop() = 0;protected:std::filesystem::path mSourceDir;mutable std::mutex mMux;};
其中,各成员函数的说明如下:
Init函数:执行初始化操作,创建mSourceDir目录并调用ConfigWatcher类的AddSource函数注册目录,同时启动线程定时拉取远端配置。
Stop函数:停止ConfigProvider。
商业版管控端配置拉取:EnterpriseConfigProvider类;
开源版管控端配置拉取:CommonConfigProvider类。
进程配置管理优化
在原有架构中,非采集配置级(即进程级和模块级)参数统一由AppConfig类进行管理,由此带来的后果包括:
AppConfig类无限增长,内部缺乏有效组织,时间一久便难以维护;
几乎所有模块都要通过AppConfig类来获取参数,因此代码中存在大量的AppConfig::GetInstance()函数,造成代码冗余和阅读不便。
有一些参数仅在商业版中使用,导致AppConfig类需要维护开源版和商业版两份,增加出现不一致的概率。
商业版代码嵌入方式优化
在原有架构中,由于类的功能界限模糊,各个类之间存在严重的依赖和耦合,因此商业版特有的功能代码散落在各个文件中,导致无法从代码库中干净剥离,只能通过文件替换的方式来完成开源和商业版代码的切换,严重影响开发效率。
组成单独的类放在单独的文件中,直接追加到开源版的目录中;
对于公共文件中的调用点,使用__ENTERPRISE__宏来控制开源和商业版的编译行为;
#ifdef __ENTERPRISE__ShennongManager::GetInstance()->Pause();#endif// ...#ifdef __ENTERPRISE__ShennongManager::GetInstance()->Resume();#endif
尽可能使用单例模式; 将开源版的类作为基类,然后将类中行为不同的方法声明为虚函数; 将商业版的类作为开源类的派生类,并重写虚函数; 在GetInstance函数中使用__ENTERPRISE__宏和指向基类的指针来控制实际生效的类; 将商业版文件直接追加到开源版的目录中;
class ProfileSender {public:static ProfileSender* GetInstance();virtual void SendToProfileProject(const std::string& region, sls_logs::LogGroup& logGroup);// other members};ProfileSender* ProfileSender::GetInstance() {#ifdef __ENTERPRISE__static ProfileSender* ptr = new EnterpriseProfileSender();#elsestatic ProfileSender* ptr = new ProfileSender();#endifreturn ptr;}
为了实现上述能力,需要对原有代码进行重组织,主要工作如下:
将与商业版配置拉取相关的代码从ConfigManager类和EventDispatcher类中剥离出来,重新组成EnterpriseConfigProvider类;
将与商业版鉴权相关的代码从ConfigManager类中剥离出来,移动到EnterpriseSLSControl类中;
将与商业版可观测数据发送相关的代码从ConfigManager类中剥离出来,移动到EnterpriseProfileSender类中;
将商业版特殊的指标监控代码从EventDispatcher类中剥离出来,重新组成ShennongManager类;
将与商业版相关的非配置级参数从AppConfig类中剥离出来,分别移动到上述新建立的类中。
option(ENABLE_ENTERPRISE "enable enterprise feature")if (ENABLE_ENTERPRISE)add_definitions(-D__ENTERPRISE__)include(${CMAKE_CURRENT_SOURCE_DIR}/enterprise_options.cmake)else ()include(${CMAKE_CURRENT_SOURCE_DIR}/options.cmake)endif ()
至此,商业版代码与开源版代码的分离工作全部完成,商业版代码文件以纯追加的方式嵌入到开源版代码中,再也不用替换文件,极大地提升了开发效率。
思考
Aliware
从决定重构之初到iLogtail 2.0形态初现,前后至少经历了大半年的时间。在传统认知里,重构是一件吃力不讨好的事情,稍不留神就会引发各种兼容性问题,甚至故障。尤其是对于iLogtail这种已有10年历史,历史包袱非常重的产品而言,进行架构升级可以说是如履薄冰。但即便如此,为什么还要坚持去做重构?一句话,长痛不如短痛。10年前的需求与现行产品定位之间的差异日益增大,强行在原有架构上继续演进只会带来更多潜在的问题,甚至于无法演进。因此,想要在可观测数据采集领域继续引领行业,在开源社区扩大影响,架构升级是一个必经的途径。
对于可观测流水线的通用概念(如数据类型和流水线定义等),iLogtail要尽量做到和领域内其它竞品保持一致,避免独树一帜给用户迁移带来不便和困惑; 对于架构实现,不能简单照搬其他主流可观测数据采集器的架构,而是在吸收其设计思想的前提下,针对iLogtail自身的特点(如双语言实现)进行原创设计,适合自己的才是最好的。 对于iLogtail的自身优势(如C++的高性能和配置热加载),在完整保留的同时,还需要将原本阻止优势发挥的限制尽可能地去除,使得自身优势能够在更多的场景中发挥作用,提升产品的核心竞争力。
每个类具体负责的功能,为后续类合并和重构奠定基础; 类间依赖,尤其是相关参数在多个类内使用的情况; 不常用的功能点,了解其预期行为,从而为补充UT作准备。
在决定进行架构升级时,笔者才从业一年,虽说有一定的C++开发经验,但是对于重构这么大的事确实没有经历过。如何保证新写的代码符合通用规范,同时又保证运行效率,是一个亟需解决的问题。
为此,在设计新架构的间隙,笔者又重新翻阅了一些经典著作,例如设计模式相关的《Design Patterns: Elements of Reusable Object-Oriented Software》,C++开发相关的《Effective C++》系列、《C++ Concurrency in Action》等书籍,同时也通过一些博客和官方文档学习C++17的新特性。与之前抱着学习的态度去阅读不同,当你带着问题和需求去重新阅读这些著作时,会更能领悟到书中一些总结性经验的实际含义。凭借着这些消化吸收后的经验,笔者一步一步打磨自己的代码,并适时对新代码进行小范围的二次重构以增强代码的复用和可扩展性。
总结
Aliware





