服务启停优化方案探讨

Java服务优雅启停实战:从原理到生产级部署指南 在分布式系统与微服务架构中,服务的生命周期管理是保障系统稳定性的基石。其核心原理在于通过进程信号机制(如SIGTERM)和框架提供的生命周期钩子(如Spring的@PreDestroy),实现应用状态的平滑过渡。这项技术的核心价值在于确保数据一致性、实现高可用发布、并提升运维效率。典型的应用场景包括日常部署更新、故障恢复、以及资源扩缩容等运维操作。本文将深入探讨如何结合**优雅关闭(Graceful Shutdown)** 与 **健康检查(Health Check)** 机制,构建从基础脚本到基于Syst 阅读详情

内容
         概览
         服务启停优化原则
         服务启停优化建议
         结束语
         附件

1. 概览
        当前服务普遍存在服务启停效率低下且易出错的问题,讨论制定一系列规则以规范相关服务启停开发,为以后进行相关设计、开发工作的同事作指导。本文主要包括服务启停优化原则、建议,以及服务启动调优的思路和参考文档。规则部分是日后开发人员在进行模块设计和编写时一定要准守的,可作为代码审查的考察点。建议的部分尽量准守,可作为服务启停优化的准则。

2. 服务启停优化原则

2.1.模块的初始化函数要有明确的出错提示

2.1.1. 规则
        模块初始化的明确出错提示可以是返回错误码、 true 或 false,也可以是 void,但是内部出错了要抛出异常,外层调用还要有 try catch 来捕获异常。对初始化成功或失败都要打印日志描述具体的错误原因,以及初始化成功。

2.1.2. 原因
        出错提示的存在保证了模块初始化/时产生的异常情况可被上层感知并处理,有利于快速分析服务的启动流程以及异常时的问题原因。

2.1.3. 反面示例
        如下代码为 CNeEntity 模块的初始化接口中的一部分。

void CNeEntity::Init()
{
ObjectVisitTreePtr pObjectTree = ObjectVisitTree::RealGet<ObjectVisitTree>();
if (!pObjectTree)
{
return;
}
. //获取网元数据
std::set<UNM_OBJECT_ID_TYPE> setNEID;
bool bReturn = pObjectTree->GetAllNEID(setNEID);
if (!bReturn)
{
return;
}
......

        该接口直接声明为了 void 类型,在初始化流程中出现任何错误均只能直接返回,且没有任何日志输出,而异常情况不能被上层感知,最终的结果就是服务启动接口实际上仅仅是调用了该模块的初始化,但无法知道初始化成功还是失败。

2.1.4. 修改示例
        针对 2.1.3 中描述的情况,需要修改接口的返回类型,出错时返回精确的错误码,并添加详细的日志输出。而在上层则可通过判断该接口的返回值来确认初始化成功还是失败,以做出相应的处理。如下所示为修改之后的代码。

long CNeEntity::Init()
{
long lErrorCode = 0;
ObjectVisitTreePtr pObjectTree = ObjectVisitTree::RealGet<ObjectVisitTree>();
if (!pObjectTree)
{
SMART_ERROR(GetNeEntityLogger(), "Get ObjectVisitTreePtr Failed!");
lErrorCode = errorcode::ET_UNM_NESERVICE::EN_NESERVER_INIT_FAIL;
return lErrorCode;
}
CNeFactoryPtr poNeFactory(new CNeFactory);
RegisterNeFactory(STR_FACTORY_NE, poNeFactory);
//获取网元数据
std::set<UNM_OBJECT_ID_TYPE> setNEID;
bool bReturn = pObjectTree->GetAllNEID(setNEID);
if (!bReturn)
{
SMART_ERROR(GetNeEntityLogger(), "GetAllNEID Failed!");
lErrorCode = errorcode::ET_UNM_NESERVICE::EN_NESERVER_INIT_FAIL;
return lErrorCode;
}
......

2.2.模块的外部调用要处理初始化的错误提示

2.2.1. 规则
        模块返回的错误码、成功或失败布尔值 false、抛出的异常外部调用都必须处理。具体如何处理,要具体情况具体分析,如该模块很重要,如果初始化失败,后面的操作都会无效,则要强制退出,或者多次尝试初始化直到该模块初始化成功;如果外部调用还有其他的模块需要初始化,可是单独开启一个线程尝试重试,当前线程继续执行其他模块的初始化。

2.2.2. 原因
        若不处理各个模块返回的错误、异常,则无法第一时间感知初始化/销毁过程中出的异常情况,当模块对应的功能出现问题时也难以排查。

2.2.3. 反面示例
        如下代码是网元服务启动过程中初始化的一个模块,该 Init 接口的返回值是 long,而此处并未处理。如此,即使下层初始化失败返回了错误码,上层也无法感知,从而导致服务出现一些难以排查的问题。

INeEntity::Init();

2.2.4. 修改示例
        修改后的代码如下,处理一下错误码,并打印日志即可。

long lErrCode = INeEntity::Init();
if (errorcode::ET_SUCCESS::EN_SUCCESS_NEW != lErrCode)
{
GetLogger().error("NeServer::OnClientInitMe ObjectTree init failed,
ErrCode = %d.", lErrCode);
stRet.SetErrorCodeValue(ERRORCODEWRAPPER_JION(errorcode::
ET_UNM_NESERVICE, EN_NESERVER_INIT_OBJECTTREE_FAIL));
return stRet;
}

2.3.模块的初始化函数要具有幂等性

2.3.1. 规则
        模块的初始化函数应该保证最多执行一次,如果异常情况下执行多次,应保证第二次及以后的执行和第一次执行没有区别,也就是初始化函数要具有幂等性,多次调用和调用一次对模块的内部数据结构和外部调用是一致的。

2.3.2. 原因
        模块重入可因模块而异导致各种各样的问题,如操作耗时、资源浪费甚至使服务直接崩溃,因此在服务初始化过程中必须避免模块重入。

2.3.3. 反面示例
        如下代码为 CNeHolderMgr 模块初始化接口的一部分。
        重点看标红的行,每次该模块初始化时都会创建并添加所有的网元对象,而网元 ID 明显是必须唯一的,这里重入的话就会导致服务直接异常崩溃,然后无限重启,无限崩溃。有如下三种情况可能导致模块重入:
        1) 该模块已由其他服务初始化成功,但当前服务再次进行了其初始化操作,导致模块重入;
        2) 该模块已由当前服务初始化成功,但该服务其他模块初始化失败导致再次调用服务初始化
接口,从而再次初始化该模块,导致模块重入;
        3) 标红的部分已初始化成功,但之后接口进入标绿的分支,导致该模块自身初始化失败,从
而上层再次调用其初始化接口,导致模块重入。

ErrorCode CNeHolderMgr::Init()
{
......
CreateObjectAndAddWhenInit(iNeID, poNe); // 该函数重入会导致失败
......
ErrorCode stCode = poNe->Init(stNeData);
If (!stCode.IsError())
{
GetLogger().error("CNeHolderMgr::Init() poNe init failed,
(%d, %d).", stCode.iType, stCode.iCode);
return stCode;
}
......
return stCode;
}

2.3.4. 修改示例
        针对 2.3.3 中阐述的三种情况,可使用如下两种方案解决:
        1. 在 Init(或者 Initialize)中表示初始化的函数中加入 m_bInit,多线程中要对 m_bInit 加锁,这样在服务初始化失败或多个服务公用一个模块情况下多次调用 Init 时,可避免多次初始化。
        2. 在 Init(或者 Initialize)中表示初始化的函数中,若初始化失败进入错误分支,需调用接口销毁该模块已有的初始化成果,以避免重新初始化可能导致的模块重入问题。若接口中存在多个分支,则可参考规则“模块初始化函数中对于包含多个步骤或分支的函数建议采用单入口-单出口原则”的方法。
        修改如下。

ErrorCode CNeHolderMgr::Init()
{
// 多线程情况下需加锁,此处仅给一个象征性的示例
mutex::scoped_lock lock(mu);
......
// 若标识为 true,表示已经初始化,直接返回成功
If (m_bInit) // 多线程访问条件下,要注意加锁控制
{
GetLogger().info("CNeHolderMgr have already inited.");
return ErrorCode(0, 0);
}
......
CreateObjectAndAddWhenInit(iNeID, poNe);
......
ErrorCode stCode = poNe->Init(stNeData);
If (!stCode.IsError())
{
// 接口异常退出时,清理已初始化的资源
ClearAllObject();
GetLogger().error("CNeHolderMgr::Init() poNe init failed,
(%d, %d).", stCode.iType, stCode.iCode);
return stCode;
}
......
// 将标识置为 true
m_bInit = true;
return stCode;
}

2.4.在线程处理函数的循环中要设置退出标识

2.4.1. 规则
        所有循环处理中,都必须设置退出标识,以便可以从循环外部终止循环语句。主要是为了解决线程无法退出,服务停不下来的问题。

2.4.2. 原因
        如果不设置退出标识,会导致服务停止时,线程无法退出等等情况发生。而设置了退出标识,在模块销毁时,通过退出标识,结合其他无效数据通知、信号量唤醒、条件变量唤醒等操作,可以快速跳出循环,完成模块销毁操作。因此,即使是要一直运行的线程,也不能 while(true)而不给出退出的条件。

2.4.3. 反面示例
        如下代码是消费者线程,可以看到,由于 while(true)而没有退出标识,这种线程处理循环,在服务停止时,如果模块等待此线程的结束,基本上是要一直等待,导致服务停止慢,或者无法停止。

//轮询
int TaskInfoQueueMgr::LoopThread()
{
while (true)
{
try
{
TaskProcessorPtr pTaskProcessorPtr;
if (GetTaskFromQueue(pTaskProcessorPtr))
{
if (NULL == pTaskProcessorPtr.get())
{
continue;
}
TaskExcuteThreadPtr pThread(new TaskExcuteThread());
pThread->SetTaskProcessorPtr(pTaskProcessorPtr);
TaskThreadMgr::get_mutable_instance().GetThreadPool().Start(pThread);
}
Sleep(1000);
}
catch (...)
{
continue;
}
}
return 0;
}

2.4.4. 修改示例
        修改后的代码如下,添加退出标识,当需要停止线程时,设置停止标识 m_bStop 为 true,即可结束线程。

//
int TaskInfoQueueMgr::LoopThread()
{
while (!m_bStop)
{
......
}
}
TaskInfoQueueMgr::~TaskInfoQueueMgr() 或者 UnInitialize()
{
m_bStop = true;
m_oThread.join();
}
或者上面例子的另一种形式:
//
int TaskInfoQueueMgr::LoopThread()
{
while (true)
{
bCanRun = xxx; // xxx 可以在外部设置(如 bCanRun = (m_pDemo != NULL))
if (!bCanRun)
{
break;
}
......
}
}

2.5.使用可中断的 Sleep 或其他替代方案进行线程等待

2.5.1. 规则
        所有需要延时处理的地方,不要使用与操作系统相关的 Sleep 进行等待。可以使用
condition.timed_wati、 time、 mutex.timed_lock、 this_thread.sleep 等替代。

2.5.2. 原因
        Sleep 进入后无法被打断,此时如需停止服务的话,在停止线程时,可能由于线程处于 Sleep 中,必须等待 Sleep 的时间到了后,线程才能继续执行,导致线程停止时间过长。

2.5.3. 反面示例
        如下代码,没循环一次,需要等待 1 秒。如果线程循环刚好开始进入 Sleep(1000)时,设置停止标志 m_bStop 为 true 的话,也需要等待 Sleep 结束,即 1 秒后,才能再次判断停止标志 m_bStop,从而退出循环。

//轮询
int TaskInfoQueueMgr::LoopThread()
{
while (!m_bStop)
{
try
{
TaskProcessorPtr pTaskProcessorPtr;
if (GetTaskFromQueue(pTaskProcessorPtr))
{
if (NULL == pTaskProcessorPtr.get())
{
continue;
}
TaskExcuteThreadPtr pThread(new TaskExcuteThread());
pThread->SetTaskProcessorPtr(pTaskProcessorPtr);
TaskThreadMgr::get_mutable_instance().GetThreadPool().Start(pThread);
}
Sleep(1000);
}
catch (...)
{
continue;
}
}
return 0;
}

2.5.4. 使用 condition.timed_wait
        修改后的代码如下,在线程处理函数的循环中,通过成员变量的 TimeWait 来实现 Sleep 的效果。如果需要停止线程时,即可设置停止标志,再使用成员变量的 NotifyAll,来快速响应,使线程处理函数退出等待,从而通过停止标志退出循环。

//
class CCustomCondition
{
public:
CCustomCondition()
{ }
~CCustomCondition()
{ }
public:
bool TimeWait(long lMillsecs)
{
boost::scoped_lock<boost::mutex> oGuard(m_oMutex);
boost::posix_time::time_duration oDuration =
boost::posix_time::milliseconds(lMillsecs);
return m_oCondition.timed_wait(oGuard, oDuration);
}
void Wait()
{
boost::scoped_lock<boost::mutex> oGuard(m_oMutex);
m_oCondition.wait(oGuard);
}
void NotifyOne()
{
boost::scoped_lock<boost::mutex> oGuard(m_oMutex);
m_oCondition.notify_one();
}
void NotifyAll()
{
boost::scoped_lock<boost::mutex> oGuard(m_oMutex);
m_oCondition.notify_all();
}
private:
boost::mutex m_oMutex;
boost::condition_variable_any m_oCondition;
};
//
void TaskInfoQueueMgr::LoopThread()
{
const int iInterval = xxx;
while (!m_bStop)
{
TaskProcessorPtr pTaskProcessorPtr;
if (GetTaskFromQueue(pTaskProcessorPtr))
{
if (NULL == pTaskProcessorPtr.get())
{
continue;
}
TaskExcuteThreadPtr pThread(new TaskExcuteThread());
pThread->SetTaskProcessorPtr(pTaskProcessorPtr);
TaskThreadMgr::get_mutable_instance().GetThreadPool().Start(pThread);
}
// 等待超时
m_obCond.TimeWait(iInterval);
}
}
//
void TaskInfoQueueMgr::Destroy()
{
if (!m_bStop)
{
m_bStop = true;
m_obCond.NotifyAll();
if (m_pThread)
{
m_pThread->join();
}
}
}

        注意事项: 模拟 Sleep 时,由于是先加锁,再进行超时等待,中间有一点点间隔,因此,多线程使用时,模拟 Sleep 等待的时间可能会有很小的误差(尽管 Sleep 本身等待的时间也不是十分精确),对精确度不做特别要求的处理中,可以忽略此误差。

2.5.5. 使用 mutex.timed_lock
        通过超时锁,当线程启动时就加锁,之后获取锁都会超时,达到 Sleep 的效果。如需退出循环时,进行解锁,等待的加锁动作就会返回,之后就可以退出线程循环了。

//
void TaskInfoQueueMgr::LoopThread()
{
while (!m_bStop)
{
// 处理
......
// 定时检查一次
boost::timed_mutex::scoped_timed_lock oIntervalLock(
m_oTimedMutex, xxx);
}
}
//
void TaskInfoQueueMgr::Start()
{
m_bStop = false;
m_oTimedMutex.lock();
}
// exit
int TaskInfoQueueMgr::Exit()
{
if (!m_bStop)
{
m_bStop = true;
m_oTimedMutex.unlock();
if (m_pThread)
{
m_pThread.join();
}
}
return 0;
}

2.5.6. 使用 boost::this_thread::sleep
        使用 boost 库中的可中断的 sleep,如果线程调用了 Interrupt 函数 ,则 sleep 会抛出
boost::thread_interrupted 异常,代码示例如下

#include <iostream>
#include <boost/thread.hpp>
#include <boost/bind.hpp>
using namespace std;
using namespace boost;
void Work()
{
cout << "thread begin..." << endl;
try
{
cout << "thread begin sleep" << endl;
boost::this_thread::sleep(boost::posix_time::hours(1));
cout << "thread sleep end" << endl;
}
catch (const boost::thread_interrupted &)
{
cout << "catch interrupted exception" << endl;
}
cout << "thread end.." << endl;
}
// 该线程主要为了实现异步中断
void Interrupt(thread *oThread)
{
cout << "interrupt thread or not (1-YES, 0-NO)" << endl;
int iCmd = 0;
cin >> iCmd;
if (1 == iCmd)
{
oThread->interrupt();
}
}
void test()
{
thread th1(&Work);
thread th2(&Interrupt, &th1);
th1.join();
th2.join();
}
int main()
{
test();
::system("pause");
return 0;
}

通过调用线程的 interrupt 函数来终止线程的 sleep,运行结果如下:中断后抛出异常,然后结束线程的执行。

2.6.如果模块有初始的接口,那么就要有销毁的接口

2.6.1. 规则
        如果模块定义了初始化接口,就要对应定义销毁接口。
2.6.2. 原因
        1、对于有线程等待的模块,如果销毁接口中无通知停止等待的操作,会导致该模块销毁不了,进而导致服务停不下来。
        2、对于有资源管理的模块,如果文件 IO、数据库连接、内存池、线程池等,如果不显示调用
销毁接口释放资源,会导致资源浪费,后续分配资源失败等问题。

2.6.3. 反面示例
        定义了统一端口的初始化接口,但没有定义销毁接口。

// 初始化函数
UNM_RPC_RETURN_CODE CRpcDispatchServerI::OnClientInitMe(const string &strTrigerOperation)
{
// 初始化统一端口
unmport::UniPortError stPortError = InitUnmPort();
if (!stPortError.IsSuccess())
{
return UNM_RPC_RETURN_CODE(stPortError.iType, stPortError.iCode);
}
return UNM_RPC_RETURN_CODE(0, 0);
}
// 销毁函数
void CRpcDispatchServerI::OnPreStop()
{
//...
// 无销毁统一端口操作
}

2.6.4. 正面示例

// 初始化函数
UNM_RPC_RETURN_CODE CRpcDispatchServerI::OnClientInitMe(const string &strTrigerOperation)
{
try
{
if (!CRetrievalFacade::Instance()->Init())
{
return UNM_RPC_RETURN_CODE(ET_TOPO_SERVER::GetErrorType(), -1);
}
}
catch (CommException &e)
{
return UNM_RPC_RETURN_CODE(e.Type(), e.Code());
}
return UNM_RPC_RETURN_CODE(0, 0);
}
// 销毁函数
void CRpcDispatchServerI::OnPreStop()
{
//...
CRetrievalFacade::Instance()->UnInit();
}


2.7.构造函数和析构函数不要做过多的逻辑处理,析构函数不要抛异常

2.7.1. 规则
        构造函数只做成员的初始化操作,不作其他逻辑操作。析构函数只做简单的资源释放操作,不应做复杂的逻辑处理。

2.7.2. 原因
        对象构造和析构如果时间过长,会导致分配资源或者释放资源过慢,引起操作失败,这类问题不容易排查。

2.7.3. 解决方案
        初始化的逻辑处理放到初始化函数 Initial 中,析构函数的逻辑处理放到 Destroy 中。

2.7.4. 反面示例
        构造函数中有反序列化操作,并且没有异常处理。

//
CNeServiceHolder::CNeServiceHolder(const CNeServiceKey &oKey, const ItemDataType &oValue)
: m_oKey(oKey)
{
m_poNeService = CNeServiceFactoryCluster::get_mutable_instance().Create(oKey.m_iType, oKey);
if (NULL != m_poNeService.get())
{
m_poNeService->PBToClass(oValue);
}
else
{
// error log
}
}

2.7.5. 正面示例
        构造函数和析构函数均为空函数,初始化和销毁的逻辑放到 Init 和 UnInit 函数中。

// 端口索引
class CRetrievalFacade
{
public:
// 获取唯一实例
static CRetrievalFacade* Instance(); // 单例模式
public:
// 初始化
bool Init();
// 清理
bool UnInit();
//...
// 清库
bool Clear();
protected:
// 构造函数 ---给成员变量赋初值
CRetrievalFacade() : m_bInited(false), ...
{ }
// 析构函数
virtual ~CRetrievalFacade()
{ }
private:
// ...... 其他成员函数
private:
// ...... 其他成员变量
};

3. 服务启停优化建议

3.1.模块初始化函数中对于包含多个步骤或分支的函数建议采用单入口-单出口原则

3.1.1. 规则
        如果函数的处理逻辑可以分为多个步骤或分支,每个步骤执行成功方可进行下一步处理,那么应该将函数所涉及的临时变量集中在函数体头部定义,返回值逻辑集中在函数体尾部实现即单入口-单出口原则。

3.1.2. 原因
        函数的处理步骤或分支较多时会造成返回值处理的位置较分散,不易于维护且容易漏掉部分步骤所涉及的资源释放。 单入口-单出口原则可以减少结果处理逻辑的代码编写,在函数退出时集中释放资源或清理脏数据。

3.1.3. 反面例子
        如下代码,函数有多处资源申请,且包含多个结果处理逻辑。

int Function()
{
int iFtpError = FTP_EXECUTE_FAILED;
FtpClientPtr pFtpClient = new FtpClient;
if (NULL == pFtpClient)
{
return FTP_EXECUTE_FAILED;
}
// 1. connect
iFtpError = pFtpClient->Connect();
if (FTP_EXECUTE_SUCCESS != iFtpError)
{
// log error
//释放
pFtpClient->Quit();
delete pFtpClient;
pFtpClient = NULL;
return iFtpError;
}
// 2. login
iFtpError = pFtpClient->Login();
if (FTP_EXECUTE_SUCCESS != iFtpError)
{
// log error
//释放
pFtpClient->Quit();
delete pFtpClient;
pFtpClient = NULL;
return iFtpError;
}
//申请资源
ResourcePtr pResource = CreateResource();
if (NULL == pResource)
{
// log error
//释放
pFtpClient->Quit();
delete pFtpClient;
pFtpClient = NULL;
return FTP_EXECUTE_FAILED;
}
// 3. upload
iFtpError = pFtpClient->Upload();
if (iFtpError != FTP_EXECUTE_SUCCESS)
{
// log error
//释放
pFtpClient->Quit();
delete pFtpClient;
pFtpClient = NULL;
delete pResource;
pResource = NULL;
return iFtpError;
}
// 4.quit
pFtpClient->Quit();
return iFtpError;
}

3.1.4. 正面示例

int Function()
{
int iFtpError = FTP_EXECUTE_FAILED;
FtpClientPtr pFtpClient = new FtpClient;
if (pFtpClient)
{
do
{
// 1. connect
iFtpError = pFtpClient->Connect();
if (FTP_EXECUTE_SUCCESS != iFtpError)
{
// log error
break;
}
// 2. login
iFtpError = pFtpClient->Login();
if (FTP_EXECUTE_SUCCESS != iFtpError)
{
// log error
break;
}
ResourcePtr pResource = CreateResource();
if (NULL == pResource)
{
// log error
iFtpError = FTP_EXECUTE_FAILED;
break;
}
// 3. upload
iFtpError = pFtpClient->Upload();
if (iFtpError != FTP_EXECUTE_SUCCESS)
{
// log error
break;
}
} while (false);
// 集中处理释放
if (Check(iFtpError))
{
// 4.quit
if (ptrFteClient != NULL)
{
ptrFteClient->Quit();
delete ptrFteClient;
}
pFtpClient->Quit();
}
DestoryResource();
}
return iFtpError;
}


3.2.根据模块与服务的强弱依赖关系选择合适的加载方式进行加载

3.2.1. 规则
        依赖性质定义:
        强依赖:子模块未初始化时会导致服务不可用。
        弱依赖:子模块未初始化时不会导致服务不可用。
        

        处理规则:
        1、 强依赖项子模块:强依赖项子模块在服务初始化过程中需要同步加载,如果该模块比较耗时,而还有其他可以并行初始化的模块,则应该开启多线程同步加载,以提高服务启动速度。
        2、 弱依赖项子模块:弱依赖项子模块在服务初始化过程中可以不立即加载,统一采用延时加载或动态加载,以加快服务启动过程。特别是所加载的数据属于大量的配置数据,优先选择动态加载,在调用时加载对应的配置。
        3、 在模块职责单一功能相对集中情况下,如加载协议文件,可考虑采用多线程加载不同的脚本文件。如果模块的加载非必须,则可以放到服务启动后,使用时加载,加载完成进行缓存,下次直接使用,以提高访问速度。

3.2.2. 原因
        子模块较多,初始化时间过长,会导致服务启动过慢,阻塞其他无依赖性的服务启动,因此需要分析子模块与服务的依赖关系,选择合适的加载方式来提高服务的启动速度。

3.2.3. 解决方案
        多线程加载:当耗时操作可以拆分成多个关联性不大的小任务时,如加载单盘的 cfg 文件,加
载端口的 xml 文件等等,可以使用多线程来进行加载,加快耗时处理,起到快速初始化的目的。
        

        启动后加载:当耗时操作很难拆分,或者拆分后关联性比较大,此时即使使用多线程加载,也要处理线程间的同步,加载速度可能提升的不是很理想。可以考虑使用启动后加载。
        

        启动后加载,又可以有两种形式:
        第一种,边加载边初始化。初始化时,启动一个线程进行耗时操作,初始化的动作继续。如果耗时操作未完成,初始化已结束,此时模块内部为半初始化状态。注意点:当耗时操作处理完成后,需要通知当前模块,将内部状态调整为已初始化状态。所有调用此模块的时候,都应该判断模块状态,如果是半初始化状态,返回未初始化完成的错误。如果是已初始化状态,继续本模块的处理。
        第二种,使用时加载。初始化时,不加载耗时操作,如单盘 cfg 协议解析等。其他模块调用本
模块的数据时,再按需加载耗时操作,并可以在此处加入缓存操作,加快之后的访问速度。注意点:这种方式,会将耗时操作分摊到初次调用的地方,虽然加快了初始化操作,但初次访
问的速度多少会有所减慢。

3.2.4. 反面例子
        如下代码是某个服务初始化过程,在初始化时服务需要加载所有单盘配置数据、从 PAS 同步采集任务、初始化业务模块、加载对象树等操作。按照依赖性的强弱,上述操作可以分为:
        1、 强依赖项:初始化业务模块、加载对象树;
        2、 弱依赖项:加载所有单盘配置数据、从 PAS 同步采集任务。

UNM_RPC_RETURN_CODE CService::OnClientInitMe()
{
UNM_RPC_RETURN_CODE stRet;
//加载单盘配置数据
std::string strPath = GetPath();
stRet = LoadBoardCfgData(strPath);
if (stRet.IsError())
{
//log error
return stRet;
}
//初始化采集任务
stRet = InitCollectTaskFromPAS();
if (stRet.IsError())
{
//log error
return stRet;
}
// 初始化业务模块
stRet = InitNeEntityDll();
if (stRet.IsError())
{
//log error
return stRet;
}
// 对象树初始化
stRet = INeEntity::Init(m_iServiceIndex);
if (stRet.IsError())
{
//log error
return stRet;
}
return stRet;
}

3.2.5. 正面示例

UNM_RPC_RETURN_CODE CService::OnClientInitMe()
{
UNM_RPC_RETURN_CODE stRet;
do
{
//利用多线程加载对象树
int iRunThread = GetThreadPool().Capacity();
for (int i = 0; i < iRunThread; ++i)
{
GetThreadPool().Start(LoadObjectTreeThread);
}
//注入回调通知对象延时加载采集任务
stRet = RegistPASClient();
if (stRet.IsError())
{
//log error
break;
}
// 初始化业务模块
stRet = InitNeEntityDll();
if (stRet.IsError())
{
//log error
break;
}
//等待多线程初始化对象树完成
GetThreadPool().JoinAll();
} while (false);
return stRet;
}
//加载单盘配置数据
UNM_RPC_RETURN_CODE CService::ProcessBoardData()
{
//do ……
//先查找是否有配置
bool bFind = FindBoardCfg(boardType);
if (!bFind)
{
//动态加载
std::string strPath = GetBoardCfgPath(boardType);
stRet = LoadBoardCfgData(strPath);
if (stRet.IsError())
{
//log error
return stRet;
}
}
//continue ……
return stRet;
}
void PASClient::Notify()
{
//通知采集任务获取完毕
pService->UpdateCollectTaskFromPAS();
}
//加载单盘配置数据
UNM_RPC_RETURN_CODE CService::UpdateCollectTaskFromPAS()
{
UNM_RPC_RETURN_CODE stRet;
//初始化采集任务
stRet = InitCollectTaskFromPAS();
if (stRet.IsError())
{
//log error
return stRet;
}
……
return stRet;
}

4. 结束语
        服务启停的快慢直接影响网管的使用体验,因此开发人员在进行模块设计时,要考虑模块初始化和销毁的效率问题,以免影响整个服务的启动和停止。本指导书制定的这些规范和建议,对于服务启停问题排查、以及优化方向提供指导,使单个服务的启动和停止时间受控,以使整个网管服务的启动和停止最优。

5. 附件

Airflow启停脚本优化:企业级自动化部署实践 本文深入探讨了企业级Airflow启停脚本的优化实践,旨在解决原始脚本在生产环境中的痛点。通过引入PID文件管理、分级停止策略、配置文件参数化、安全权限控制及Systemd服务集成,将脚本从“能用”升级为可靠、安全、灵活的自动化部署组件,并融入CI/CD流水线,实现多环境适配与高效运维。 阅读详情

相关推荐

2021.11.23 Covins 多机器人单目协同建图 跑Euroc数据集 更新

近日,一直在调试如何使用Covins跑通数据集,因为是初学者,所以很多bug出来的时候原理都不懂,只能复制——百度——尝试解决的过程。 现分享一下近日的结果以及与Covins作者帮助解答的一些问题。 使用代码行跑依次跑Euroc多数据集 这里的意思是使用作者提供的covins_examples文件夹中的euroc_examples_mh123_vigba.sh文件,这个文件是将三个Euroc数据集依次运行,跑完第一个数据集,第二个数据集运行的时候在第一个数据集生成地图的基础上继续绘制,由于我只下载了两个数据

qq_42614598的博客 2473

boost互斥锁_boost-同步-互斥量的概念

互斥对象有利于实现多线程中数据的线程安全。线程调用锁函数来获得互斥对象的所有权,调用对应的解锁函数来放弃所有权。互斥量可以是递归或非递归的,并且可以同时把所有权赋给多个线程。Boost.Thread提供独占的递归、非递归互斥量,以及共享(多读单一写)互斥量。BasicLockable概念BasicLockable是独占模型。满足这个模型的对象需要实现两个接口m.lock()m.unlock()调用...

weixin_39758712的博客 185

IBM Storwize V3700存储维护实战:硬盘更换、电源模块热插拔与带外管理IP配置

在企业IT基础设施中,存储阵列是数据持久化的核心组件,其稳定运行直接关系到业务连续性。存储阵列通常采用RAID技术实现数据冗余,并通过热插拔、冗余电源等设计保障高可用性。带外管理则提供了独立于业务网络的硬件级监控与管理通道,是运维的关键手段。本次实战以一台IBM Storwize V3700存储阵列为例,详细记录了在业务低峰期,依据严谨的操作流程,对出现SMART预警的硬盘进行在线更换,并对有异响的冗余电源模块实施热插拔更换。针对管理界面强制要求设置VIP才能配置带外管理地址的限制,文章提供了通过SSH命令

weixin_29172963的博客 303

boost--线程同步

1、互斥锁(互斥量) mutex是独占式的互斥锁。timed_mutex增加了超时功能。 成员函数:lock()用于锁定,try_lock()为非阻塞版本的锁定,unlock()用于解锁。timed_lock()只属于timed_mutex,它可以等待一定的时间,等待的时间可以是一个时间段,也可以是指定的时间。 使用方法:使用mutex必须配合try-catch块以保证解锁互斥...

weixin_30415113的博客 223

【Cadence】 ADS Dynamic Link使用教程

在Cadence617上使用ADS Dynamic Link

乾巽的博客 6072

01 Aras Innovator二次开发实战:从IOM到AML的完整交互指南

本文详细介绍了Aras Innovator二次开发实战,从IOM到AML的完整交互指南。通过JavaScript和C#代码示例,展示了如何在客户端和服务端使用IOM进行开发,并深入解析AML语言的基础语法和调试技巧。文章还提供了文档管理和BOM管理等典型场景的实战案例,帮助开发者快速掌握Aras Innovator二次开发的核心技术。

wind6的博客 650

【C#】Windows服务(Service)安装及启停

今天,我们完成了一个应用程序的开发,我们要将其设置成开机自启程序,继上篇文章介绍有两种方式。一种,开机启动目录下创建应有程序的快捷方式;另一种,写Windows服务;我们现在采用“服务方式自启程序”。因为用户不具备管理员权限,为了安全考虑,我们方便监控管理。最终确认方案二。先说说我们面临的问题,或者说是待解决的问题:①安装服务,通过咱新开发的C#应用软件安装,不希望借助第三方工具(installutil.exe),因为面向人群非科班出身,操作越简单越好。

花北城的博客 1万+

华为私有云HCS 8.1.1实战:从镜像制作到云主机发放全流程指南

本文详细解析了华为私有云HCS 8.1.1从镜像制作到云主机发放的全流程实战指南。内容涵盖制作标准化QCOW2镜像、配置云主机规格与主机组、规划外部网络,以及在运营面为租户初始化资源、配置审批流与产品目录。最后,通过用户视角演示了从创建VPC网络到申请、审批并最终绑定EIP访问云主机的完整操作步骤,为运维人员和架构师提供了一套清晰、可落地的HCS部署与管理实践方案。

weixin_29305313的博客 1124

boost C++知识点(四)

5 多线程 5.1 线程管理 在这个库最重要的一个类就是 boost::thread,它是在 boost/thread.hpp 里定义的,用来创建一个新线程。下面的示例来说明如何运用它。 #include &amp;lt;boost/thread.hpp&amp;gt; #include &amp;lt;iostream&amp;gt; void wait(int seconds) { boost::this_t......

qq_23350817的博客 957

2、探索谷歌云平台(GCP):入门指南

本文全面介绍了谷歌云平台(GCP)的入门知识,涵盖其全球基础设施、核心服务和使用方法。内容包括GCP区域与可用区结构、创建免费账户、配置计算资源(如GCE虚拟机)、存储类型(PD、Filestore、GCS)、网络架构(VPC)、组织与项目管理、身份访问控制(IAM),以及计算、存储、数据库、大数据和人工智能等各类云服务。文章还提供了资源规划、安全管理和成本控制等实用建议,帮助用户快速上手并高效利用GCP构建可扩展的云解决方案。

uber9的博客 254

Python 爬虫实战:爬取哔哩哔哩知识区,抓取高赞课程信息

哔哩哔哩(Bilibili)是一个广受欢迎的综合性视频社区,其中的知识区汇聚了大量优质的教育、科普、技术等课程内容。本文将指导读者如何使用 Python 爬虫技术从哔哩哔哩知识区抓取高赞课程信息,并进行数据分析。本文深入探究了利用 Python 爬虫技术从哔哩哔哩知识区抓取高赞课程信息的全流程。从前期的环境搭建、模拟登录,到核心的课程数据抓取、存储,再到后续的数据分析与可视化,都详细展开讲解并提供代码支持。通过此方法所获取的数据,能够助力创作者优化内容、用户发现优质课程、平台洞察用户需求。

u014481728的博客 1133

boost::serialization::singleton单例的使用方式

class CDerived:public boost::serialization::singleton // 只允许单个实例,不能复制、赋值(基类派生自boost::noncopyable)// static先调用由boost提供的get_mutable_instance()(或get_const_instance())再调用成员函数的方式。#define sCTest_const singleton_ctest::get_const_instance() // const实例。

XUZHENG19860228的专栏 684

QT实现图片ROI区域的框选、移动、缩放(附完整源码)

QT实现图片ROI区域的框选、移动、缩放(附完整源码)

希望我的博客,能帮上你解决学习中工作中所遇到的问题 1191

Spring Boot 的优雅启停:确保停机不影响交易

多中心并行发版:各中心并行升级,确保始终有可用实例。服务下线前的流量切换:使用服务注册中心(如 Eureka)将待下线实例置为“DOWN”。进程关闭信号选择:使用 kill -15(SIGTERM)触发优雅关闭,而非kill -9(SIGKILL)。线程池的优雅停止:配置线程池等待任务完成,避免强制中断。Spring Boot 2.3 内置优雅关机:启用,设置超时时间。启动时的优雅准备:检测依赖、初始化资源,确保服务就绪后再接受流量。优雅启停是系统稳定运行的关键。

sc35262的博客 1170

Devexpress16.2.6破解工具

Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具Devexpress16.2.6破解工具

jdk-8u144-linux-x64.tar.gz.

该资源为搭建Linux系统Java环境用,资源的后缀为tar.gz格式的压缩包

SiameseUIE成本优化:按需启停服务降低云实例持续运行开销

本文介绍了如何在星图GPU平台上自动化部署SiameseUIE 模型部署镜像 README,实现轻量级中文文本信息抽取。该镜像支持按需启动、秒级响应,典型应用于电商评论中人物与地点实体的实时识别,显著降低云资源持续运行成本。

weixin_42524864的博客 286

Data-Structures-and-Algorithm-Analysis-in-Java.pdf

作者: Weiss, Mark A. 出版社: Addison Wesley Data Structures and Algorithm Analysis in Java is an “advanced algorithms” book that fits between traditional CS2 and Algorithms Analysis courses. In the old ACM Curriculum Guidelines, this course was known as CS7. This text is for readers who want to learn good programming and algorithm analysis skills simultaneously so that they can develop such programs with the maximum amo...

泵站远程启停

随着物联网技术的迅猛发展,传统泵站的管理方式正面临前所未有的变革。在这一变革的浪潮中,HiWoo Cloud平台凭借其卓越的技术实力和创新理念,为泵站远程启停控制带来了全新的解决方案。

weixin_50072277的博客 617

怀旧版电脑GBC模拟器

再现原版GBC掌机的界面,电脑运行连游戏界面都一样大小,很怀旧,很真实,(英文版)强烈推荐,很小的文件,很难才找到的。

SmartDNS启动冲突:从系统日志看服务启停优化

深夜两点,网络运维工程师小李接到紧急电话:公司OpenWRT路由器重启后完全死机,所有网络服务中断。经过排查,发现问题出在新安装的SmartDNS服务上。每次系统重启,SmartDNS的启动脚本就会陷入无限循环,最终导致整个系统崩溃。 ## 故障现场重现 打开etc/init.d/smartdns文件,我们发现了两个关键问题区域: **启动等待逻辑缺陷** ```bash while tru

gitblog_00403的博客 837

Qwen3-VL长期运行方案:云端自动启停,用多少付多少

精准控制成本:只在需要时付费,实测可节省60%以上费用多种触发方式:既支持固定时段,也能根据使用情况自动启停灵活调整策略:随时修改cron规则或空闲检测阈值资源优化组合:配合量化和合适GPU选择进一步降本现在就可以在优快云算力平台部署你的Qwen3-VL服务,开始体验智能启停带来的成本优势!💡获取更多AI镜像想探索更多AI镜像和应用场景?访问优快云星图镜像广场,提供丰富的预置镜像,覆盖大模型推理、图像生成、视频生成、模型微调等多个领域,支持一键部署。

AmberTiger47的博客 1016

人大金仓KCA | KingbaseES的安装与启停

KES是一款为企事业单位管理信息系统、业务及生产系统、决策支持系统等量身打造的承载数据库,致力于解决高并发、高可靠的数据存储计算问题,具有“三高”(高性能、高可用、高安全)、“三易”(易管理、易使用、易迁移)、运行稳定等特性,是真正具有高成熟度的数据库产品,在市场竞争中对标 0racle、SQL Server、DB2、MySQL等OLTP 类产品。

深耕信创・网络・云原生领域 3095

一键启停Redis:Linux系统下的自动化脚本秘籍

在Linux系统中,Redis服务的启动和停止通常需要手动输入命令。为了提高效率,我们可以通过编写一个shell脚本来实现一键启停Redis服务。

我先测了 1万+

【Docker DevOps效率革命】:如何用v2 profile语法精准控制服务启停?

掌握Docker Compose v2的扩展字段与profile新语法,精准控制服务启停。适用于多环境部署与开发调试,通过定义profiles实现按需启动,提升资源利用率与运维效率。配置灵活,语法简洁,DevOps实践利器,值得收藏。

QuickProceed的博客 891

从0到1部署MailDrop:Linux环境下的Redis配置、服务启停与监控技巧

想要搭建自己的私有邮件服务吗?MailDrop是一个开源、可扩展的高性能邮件服务系统,类似于Mailinator的替代方案。本文将为您详细介绍如何在Linux环境下从零开始部署MailDrop,包括Redis配置优化、服务启停管理以及系统监控技巧。无论您是个人开发者还是企业运维人员,这份完整指南都能帮助您快速搭建稳定的邮件服务环境。 ## 一、环境准备与系统要求 在开始部署MailDrop之前

gitblog_00795的博客 1001

工厂设备远程启停

在快速发展的工业4.0时代,工厂设备的智能化、远程化操控已成为企业提升生产效率、降低运营成本的关键。HiWoo Cloud平台,凭借其卓越的设备远程启停能力,为企业提供了高效、便捷的智能化解决方案。

weixin_50072277的博客 521

DeepSeek-R1长期运行方案:云端GPU+自动启停,省心省钱

本文介绍了基于星图GPU平台自动化部署DeepSeek-R1-Distill-Qwen-1.5B镜像的完整方案,实现AI模型的低成本长期运行。通过云端GPU实例与自动启停机制,可高效支持模型微调、智能问答等场景,兼顾性能与经济性,助力开发者快速构建稳定的AI应用服务。

BlackironLynx23的博客 177

wan2.1-vae GPU成本优化:按需启停服务+定时释放显存,月均GPU费用降低45%

本文介绍了如何在星图GPU平台上自动化部署wan2.1-vae镜像,实现AI图像生成功能。该镜像基于Qwen-Image-2512模型,能够根据中英文提示词生成高质量图像,适用于电商设计、创意内容制作等场景。通过按需启停服务和定时释放显存,可显著降低GPU使用成本。

weixin_32661831的博客 745
上一篇: curl操作之Get/Post/上传文件 参数设置以及调用细节
下一篇: QUIC原理及使用
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ปรัชญา แค้วคำมูล

你的鼓励将是我创作的最大动力!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值