微服务开源框架TARS 之 框架服务解析
作者 herman 简介 本文源自herman的系列文章之一《鹅厂开源框架TARS之运营服务监控》。相关代码已按TARS开源社区最新版本更新。 TARS框架为用户提供了涉及到开发、运维、以及测试的一整套解决方案,帮助一个产品或者服务快速开发、部署、测试、上线。 它集可扩展协议编解码、高性能RPC通信框架、名字路由与发现、发布监控、日志统计、配置管理等于一体。其中发布监控,日志统计等运维功能依靠着TARS框架中的运维服务和工具,本文将对各运维服务的功能和作用进行分析,并通过部分源码帮助读者进一步理解TARS的工作原理。 目录 运维工具概览 TarsWeb Registry服务 Patch服务 Config服务 Log服务 Stat服务 Property服务 Notify服务 Node服务 运维工具概览 如上图,可以清楚看到TARS框架的运维服务和工具:包括主控 Registry 、发布平台 Patch 、配置文件中心Config 、远程 Log 、指标统计 Stat 、业务信息 Property 、异常信息 Notify 等主要服务,再结合 TarsWeb 平台对这些服务进行可视化操作和运用,对开发和运维人员算是非常方便和人性化了。 TarsWeb —— 管理平台 TarsWeb 服务管理平台用于服务的管理与运维,功能包括: 提供服务状态信息查询和起停服务、设置服务日志级别、发送自定义命令等操作页面 提供部署服务、自动编译发布、配置管理等运维操作页面 提供自动测试操作界面 展示服务性能指标数据 展示业务特性指标数据 TarsWeb 可视化管理平台对服务的管理和运维功能都是基于TARS框架运维服务的接口来提供服务的。 Registry服务 —— 主控服务 Registry 服务提供对象名称寻址服务,返回 IP:Port 列表,为客户端提供可用服务列表信息。 同时,它提供TARS框架核心管理功能 :服务部署、服务起停、服务状态信息查询、发布、配置管理、命令通知。 Registry 提供的接口有 registerNode(node节点注册) keepAlive(node上报心跳负载) getServers(获取在该node部署的server列表) updateServer(更新server状态) 同时主控提供了 AdminRegImp (关联控制接口类),提供如下接口: getAllApplicationNames(获取application列表) getAllNodeNames(获取node列表) shutdownNode(停止 node) getServerState(获取特定server状态) startServer(启动特定server) restartServer( 重启特定server) notifyServer(通知服务) patchServer(发布特定server) 下面来看看 startServer 接口如何实现。 startServer 接口的实现中,先调用 updateServerState 更新服务的状态 //更新数据库server的设置状态 DBPROXY->updateServerState(application, serverName, nodeName, "setting_state", tars::Active); 然后判断 server 是否为 DNS:DNS直接通过db修改状态,非DNS则通过Node节点启动服务(下节介绍Node节点) if(server.size() != 0 && server[0].serverType == "tars_dns") { TLOGINFO(" '" + application + "." + serverName + "_" + nodeName + "' is tars_dns server" << endl); iRet = DBPROXY->updateServerState(application, serverName, nodeName, "present_state", tars::Active); } else { // 获取Node节点proxy NodePrx nodePrx = DBPROXY->getNodePrx(nodeName); TLOGINFO("call node into " << __FUNCTION__ << "|" << application << "." << serverName << "_" << nodeName << "|" << current->getHostName() << ":" << current->getPort() <<endl); current->setResponse(false); NodePrxCallbackPtr callback = new StartServerCallbackImp(application, serverName, nodeName, current); // 异步启动服务 nodePrx->async_startServer(callback, application, serverName); } Patch服务 —— 统一发布 Patch服务提供服务的发布功能,用于实现服务发布包的上传、管理与发布,配合TarsWeb平台,能够管理所有需要发布的服务和文件的目录,如下 Patch 服务中定义了以下四个接口 /** * 获取路径下所有文件列表信息 * @param path, 目录路径, 相对_directory的路径, 不能有.. * @param vector<FileInfo>, 文件列表信息 * @return int */ int listFileInfo(const string &path, vector<FileInfo> &vf, TarsCurrentPtr current); /** * 下载文件 * @param file, 文件完全路径 * @param pos, 从什么位置开始下载 * @return vector<byte>, 文件内容 */ int download(const string &file, int pos, vector<char> &vb, TarsCurrentPtr current); /** * 准备好需要patch的文件,将发布的文件从上传目录复制到发布目录 * @param app, 应用名 * @param serverName, 服务名 * @param patchFile, 需要发布的文件名 * @return int, 0: 成功, <0: 失败 */ int preparePatchFile(const string &app, const string &serverName, const string &patchFile, TarsCurrentPtr current); /** * 删除patch文件 * @param app * @param serverName * @param patchFile * @param current * @return */ int deletePatchFile(const string & app, const string & serverName, const string & patchFile, TarsCurrentPtr current); Config 服务 —— 配置中心 Config 服务用于提供整套框架的配置文件保存和读取等操作,后台使用mysql存储。配置拉取服务化,服务只需调用配置服务的接口即可获取到配置文件。 为了能灵活管理配置文件,配置文件分为几个级别:应用配置、Set配置、服务配置和节点配置。 应用配置为最高一级的配置文件,它是多个服务配置提炼出来的公共配置,服务配置通过引用它来使用其配置内容。 Set配置是具体一个Set分组下所有服务的公共配置,在应用配置的基础上进行补充追加。 服务配置是具体一个服务下所有节点的公共配置,可以引用应用配置。 节点配置是一个应用节点的个性化配置,它和服务配置合并成为具体一个服务节点的配置。 下图是服务配置管理页面 添加配置的实现 在服务业务代码中,可以通过调用 addAppConfig 和 addConfig 来分别添加应用级配置文件和服务配置文件。接下来我们来对 addAppConfig 和 addConfig 的添加配置的过程进行分析。 addAppConfig addAppConfig("DBConnection.conf"); //添加应用级别的配置文件 addAppConfig 中会调用 RemoteConfig::getInstance()->addConfig(filename, result, true) 函数; 然后该函数会调用 getRemoteFile 函数从 Config 服务远程获取配置文件信息。 getRemoteFile 函数中,通过 _configPrx 向 Config 服务发起 rpc 调用实现,如下 _configPrx->loadConfig(_sApp, (bAppConfigOnly ? "" : _sServerName), sFileName, stream); ConfigImp::loadConfig 函数是 Config 的接口函数 ,如果是应用级别的配置文件,则执行 select id,config from t_config_files... 并且把结果使用string 的方式回传给调用者。 调用者获取到文件内容,根据文件的名字生成在本地以供后续程序使用文件: std::ofstream out(newFile.c_str()); addConfig addConfig(ServerConfig::ServerName + ".conf"); 和 addAppConfig 相似, addConfig 也会调用 RemoteConfig::getInstance()->addConfig(filename, result, false) 。最后一个参数 bAppConfigOnly 设置为 false ,表示获取服务配置,而不是应用配置。 接着进行 rpc 调用 int ret = _configPrx->loadConfig(_sApp, (bAppConfigOnly ? "" : _sServerName), sFileName, stream); 和加载应用级别的配置文件有所不同,这里调用 _configPrx 服务器的参数,增加了 _sServerName,即 rpc 调用 loadConfig 接口的时候 server 参数不为空字符串,会改调用 loadConfigByHost 函数 ... if(!server.empty()) { return loadConfigByHost(app + "." + server, fileName, current->getHostName() , config, current); } else { return loadAppConfig(app, fileName, config, current); } loadConfigByHost 函数同样是通过mysql查询 t_config_files 表格,但修改了 where 查询条件,appName 替换为 appServerName,即查询服务配置,如下 ... "where server_name = '" + _mysqlConfig.escapeString(appServerName) + "' " ... 至此,实现了从指定服务器拉取配置文件的功能 (这里还关系到引用配置等功能,多个同名文件还涉及到文件合并等,这里先不做详细说明)。 Log服务 —— 日志中心 TARS框架的日志服务,用于接收远程日志。 提供两个接口,如下 /** * 输出日志信息到指定文件 * @param app 业务名称 * @param server 服务名称 * @param file 日志文件名称 * @param format 日志输出格式 * @param buffer 日志内容 */ void logger(const string &app, const string &server, const string &file, const string &format, const vector<string> &buffer, tars::TarsCurrentPtr current); /** * 获取数据 * @param info * @param buffer */ void loggerbyInfo(const LogInfo & info,const vector<std::string> & buffer,tars::TarsCurrentPtr current); 业务服务以框架层的 API 异步发送日志到日志服务器,例如 Stat 服务的 ReapSSDThread::run 中通过 FDLOG 发送日志 FDLOG("CountStat") << "stat ip:" << ServerConfig::LocalIp << "|Buffer Index:" << iBufferIndex << "|ReapSSDThread::run insert record num:" << iTotalNum << "|tast patch finished." << endl; 在TarsWeb上可以查询到相应的日志 FDLOG 的定义在库文件的 RemoteLogger.h ,定义如下: #define FDLOG(x) (RemoteTimeLogger::getInstance()->logger(x)->any()) RemoteTimeLogger::getInstance()->logger(x) 函数生成并返回:TimeLogger*,TimeLogger 的定义如下: typedef TC_Logger<TimeWriteT, TC_RollByTime> TimeLogger; 其中 TC_Logger 为日志基类模板:模板第一个参数 TimeWriteT 负责写Logger。在 applicantion 服务启动的时候会调用设置远程日志服务器对象的服务,例如 log=tars.tarslog.LogObj,然后调用 setLogInfo 设置本地信息 RemoteTimeLogger::getInstance()->setLogInfo(_communicator, ServerConfig::Log, ServerConfig::Application, ServerConfig::ServerName, ServerConfig::LogPath,setDivision()); 这里底层实现函数如下,会获取远程日志服务器的地址 void RemoteTimeLogger::setLogInfo(const CommunicatorPtr &comm, const string &obj, const string &sApp, const string &sServer, const string &sLogpath, const string& setdivision, const bool &bLogStatReport) { _app = sApp; _server = sServer; _logpath = sLogpath; _comm = comm; _setDivision = setdivision; _logStatReport = bLogStatReport; if(!obj.empty()) { _logPrx = _comm->stringToProxy<LogPrx>(obj); //单独设置超时时间 _logPrx->tars_timeout(3000); if(_defaultLogger) { _defaultLogger->getWriteT().setLogPrx(_logPrx); } } //创建本地目录 TC_File::makeDirRecursive(_logpath + "/" + _app + "/" + _server); } 具体写日志的时候,TimeWriteT 类重载了 operator() ,会调用远程日志服务的logger 接口写远程日志,从而实现了日志从本地到远程日志服务器的功能 void TimeWriteT::operator()(ostream &of, const deque<pair<size_t, string> > &buffer) { ... _logPrx->logger(DYEING_DIR, DYEING_FILE, "day", "%Y%m%d", vDyeingLog, ServerConfig::Context); ... } Stat服务 —— 性能指标统计 Stat 服务用于监控服务进程的运行质量,提供服务模块间调用信息统计上报的功能。Stat采集的数据包含 服务的性能数据,包括:调用时间、成功次数、超时次数、异常次数、耗时分布等信息。 服务间的调用关系链采样,包括:主调、被调、接口名等信息,以便定位问题服务。 以下为 Stat 服务定义的接口: /** * 上报模块间调用信息 * @param statmsg, 上报信息 * @return int, 返回0表示成功 */ virtual int reportMicMsg( const map<tars::StatMicMsgHead, tars::StatMicMsgBody>& statmsg, bool bFromClient, tars::TarsCurrentPtr current ); /** * 上报模块间调用采样信息 * @param sample, 上报信息 * @return int, 返回0表示成功 */ virtual int reportSampleMsg(const vector<StatSampleMsg> &msg,tars::TarsCurrentPtr current ); Property服务 —— 业务指标统计 Property服务提供用户自定义属性上报功能,用于监控业务的运行质量情况和相关指标的统计。采集的数据包含 服务业务特性数据,包括内存大小、队列大小、cache命中率等;支持平均、计数、求和、分布等统计方式。 Property服务提供接口 reportPropMsg 进行服务特性上报,接口声明如下 /** * 上报属性信息 * @param statmsg, 上报信息 * @return int, 返回0表示成功 */ virtual int reportPropMsg(const map<StatPropMsgHead,StatPropMsgBody>& propMsg, tars::TarsCurrentPtr current ); Notify服务 —— 异常信息 官方文档定义为异常信息,用于获取服务的业务异常上报的report,输出的信息可以在TarsWeb平台里面看到。 具体服务业务代码中,通过获取 RemoteNotify 实例调用 report 上报异常信息,例如: RemoteNotify::getInstance()->report("This is a report test"); 服务管理界面回显示服务上报的异常信息,如下 源码实现主要部分如下: if(_notifyPrx) { ReportInfo info; info.eType = REPORT; info.sApp = _app; info.sServer = _serverName; info.sSet = _setName; info.sThreadId = TC_Common::tostr(std::this_thread::get_id()); info.sMessage = sResult; info.sNodeName = _nodeName; if(!bSync) { _notifyPrx->async_reportNotifyInfo(NULL, info); } else { _notifyPrx->reportNotifyInfo(info); } } Notify 服务的实现,主要就是把数据插入 t_server_notifys_ 数据库。这里默认都是使用异步的接口,否则效率可能会有问题,毕竟是直接操作db,相关操作源码如下 ... string sql; TC_Mysql::RECORD_DATA rd; rd["application"] = make_pair(TC_Mysql::DB_STR, info.sApp); rd["server_name"] = make_pair(TC_Mysql::DB_STR, info.sServer); rd["container_name"] = make_pair(TC_Mysql::DB_STR, info.sContainer); rd["server_id"] = make_pair(TC_Mysql::DB_STR, info.sApp + "." + info.sServer + "_" + nodeId); rd["node_name"] = make_pair(TC_Mysql::DB_STR, nodeId); rd["thread_id"] = make_pair(TC_Mysql::DB_STR, info.sThreadId); if (!info.sSet.empty()) { vector<string> v = TC_Common::sepstr<string>(info.sSet, "."); if (v.size() != 3 || (v.size() == 3 && (v[0] == "*" || v[1] == "*"))) { TLOGERROR("NotifyImp::reportNotifyInfo bad set name:" << info.sSet << endl); } else { rd["set_name"] = make_pair(TC_Mysql::DB_STR, v[0]); rd["set_area"] = make_pair(TC_Mysql::DB_STR, v[1]); rd["set_group"] = make_pair(TC_Mysql::DB_STR, v[2]); } } else { rd["set_name"] = make_pair(TC_Mysql::DB_STR, ""); rd["set_area"] = make_pair(TC_Mysql::DB_STR, ""); rd["set_group"] = make_pair(TC_Mysql::DB_STR, ""); } rd["result"] = make_pair(TC_Mysql::DB_STR, info.sMessage); rd["notifytime"] = make_pair(TC_Mysql::DB_INT, "now()"); string sTable = "t_server_notifys"; try { _mysqlConfig.insertRecord(sTable, rd); } ... Node服务 —— 节点管理 服务节点可以认为是服务所实际运行的一个具体的操作系统实例,可以是物理主机或者虚拟主机、云主机。每台服务节点上均有一个Node服务和多个业务服务,Node服务会对业务服务进行统一管理,包括: 同一节点(容器、服务器、虚拟机等)上的服务起停、服务状态信息采集、发布、配置管理、自定义消息通知。 同一节点上的服务监控,异常退出、僵死等监控重启。 Node 服务提供的主要接口: patch(patch指定服务) destroyServer (销毁指定服务) shutdown (关闭Node) stopAllServers (停止所有服务) loadServer (加载指定服务) startServer (启动指定服务) stopServer (停止指定服务) notifyServer (通知服务) Registry 服务的 startServer 会调用本节中 Node 服务的 startServer 接口,startServer 再通过调用 CommandStart 类的 startByScript 函数实现服务的启动,该派生自 ServerCommand ,同理也有 CommandStop, CommandPatch 等,具体详见源码。 startByScript 通过拉取服务的启动脚本启动服务,启动脚本一般随服务conf存储在DB中,然后调用以下函数执行脚本启动服务 _serverObjectPtr->getActivator()->activate(sStartScript, sMonitorScript, sResult); 最终通过C语言exec系列函数执行启动脚本启动服务。 除了以上主要的管理服务的接口外,还有用于获取Node服务信息的接口,如 getState 用于获取指定服务状态,实现如下: ServerState NodeImp::getState( const string& application, const string& serverName, string &result, TarsCurrentPtr current ) { string serverId = application + "." + serverName; result = string(__FUNCTION__)+" ["+serverId+"] "; ServerObjectPtr pServerObjectPtr = ServerFactory::getInstance()->getServer( application, serverName ); if ( pServerObjectPtr ) { result += "succ"; return pServerObjectPtr->getState(); } result += "server not exist"; NODE_LOG(serverId)->error() << "NodeImp::getState " << result << endl; return tars::Inactive; } 总结 本文介绍分析了TARS框架中如何通过不同运维服务工具实现对服务的运营和监控,为开发和运维人员提供方便、人性化的服务管理和维护功能。 TARS可以在考虑到易用性和高性能的同时快速构建系统并自动生成代码,帮助开发人员和企业以微服务的方式快速构建自己稳定可靠的分布式应用,从而令开发人员只关注业务逻辑,提高运营效率。多语言、敏捷研发、高可用和高效运营的特性使 TARS 成为企业级产品。 TARS微服务助您数字化转型,欢迎访问: TARS官网:https://TarsCloud.org TARS源码:https://github.com/TarsCloud 获取《TARS官方培训电子书》:https://wj.qq.com/s2/6570357/3adb/ 或扫码获取: