1. 项目概述与核心价值最近在做一个后台服务的性能监控模块其中一个核心需求是记录关键路径上每个操作的耗时精度要求达到微秒级并且要能应对系统时间被意外调整比如NTP同步、手动修改系统时间的情况。这听起来简单但真动起手来发现坑不少单线程写日志在高频操作下会成为瓶颈而时间跳变如果处理不好轻则导致耗时计算出现负数重则让基于时间序列的分析完全失真。所以我决定动手撸一个多线程版本的高精度时间日志记录器核心目标就两个快和准。快意味着日志写入不能阻塞主业务线程准意味着记录的时间戳必须真实可靠能免疫系统时间的突变。这个项目非常适合那些正在开发高性能服务器、金融交易系统、实时数据处理平台或者任何对操作时序和耗时极度敏感的后台服务的C开发者。如果你也在为“打点”日志拖慢程序速度或者发现日志里的时间线“穿越”了而头疼那接下来的内容应该能给你一套可以直接拿去用的解决方案。我会从设计思路、核心实现、多线程同步、时间跳变检测的原理和避坑经验完整地过一遍。2. 整体架构与设计思路拆解2.1 核心需求与挑战分析首先我们得把需求掰开揉碎了看。所谓“高精度时间日志记录”不仅仅是调用一下std::chrono::system_clock::now()那么简单。我们需要的是一个能够持续、稳定、低开销地产生时间戳并将其与事件信息一起持久化的机制。而“时间跳变检测”则是这个机制的保险丝防止因外部因素导致的时间回溯或飞跃污染我们的数据。主要的挑战来自四个方面性能开销获取高精度时间戳本身如使用std::chrono::steady_clock或clock_gettime有一定成本。如果在业务逻辑的热路径上同步执行获取时间戳、格式化字符串、写入文件这一系列操作累积的开销会非常可观。线程安全多线程环境下多个线程同时调用日志记录接口如何保证日志消息不乱序、不丢失、不交叉写入一条日志被另一个线程的消息打断I/O瓶颈文件写入是典型的慢速I/O操作。同步写文件会阻塞调用线程成为整个系统的性能瓶颈。时间跳变系统时钟可能因为NTP同步、用户手动修改、虚拟机热迁移等原因发生向前或向后的突变。使用system_clock会直接受到影响产生时间戳倒流或突进。这对于计算耗时、基于时间窗口的统计都是灾难性的。2.2 多线程生产者-消费者模型选型为了解决上述挑战最经典的架构就是生产者-消费者模型。在这个模型里生产者各个业务线程。它们只负责产生原始的日志消息包含事件内容和高精度时间戳并将其放入一个缓冲区队列。这个操作要尽可能快最好是内存操作。消费者一个或多个专用的后台日志线程。它们负责从队列中批量取出日志消息进行格式化、组装等耗时操作最后写入文件。这样做的好处显而易见解耦业务线程生产者和日志I/O消费者异步执行业务线程不会被慢速的磁盘I/O阻塞。批处理消费者可以一次从队列中取出多条日志进行一次批量写入显著减少I/O次数提升磁盘利用率。流量削峰短时间内产生的大量日志可以被队列缓冲避免直接冲击I/O系统。对于队列的选择我们通常使用一个线程安全的阻塞队列。C标准库没有现成的我们可以用std::queue或std::deque配合std::mutex和std::condition_variable自己实现一个也可以考虑使用像moodycamel::ConcurrentQueue这样的高性能第三方无锁队列。在初始版本中为了清晰展示原理我会使用“互斥锁条件变量”实现一个简单的阻塞队列。2.3 时间源的选择与跳变检测原理这是本项目技术上的核心难点。时间源的选择直接决定了日志时间戳的“质量”。std::chrono::system_clock代表系统壁钟时间可以被修改。它的时间点对应真实的日历时间但会跳变。不能单独用于计算耗时和保证单调递增。std::chrono::steady_clock代表单调时钟保证其now()的调用返回值永远不会减少且以均匀的速率前进。它是计算时间间隔耗时的理想选择但其时间点与日历时间无关不能直接转换为人类可读的日期时间。std::chrono::high_resolution_clock通常是system_clock或steady_clock的别名精度可能更高但单调性没有保证慎用。我们的策略是“双时钟源主从校验”主时间戳用于日志输出和外部对齐使用system_clock。因为它能转换为time_t进而格式化为可读的日期时间这对于日志排查至关重要。从时间戳用于内部排序和跳变检测使用steady_clock。它提供稳定、单调递增的时间基准不受系统时间修改的影响。跳变检测逻辑每次记录日志时同时获取system_clock的S_now和steady_clock的T_now。我们维护一个“锚点”在程序启动时记录初始的S_base和T_base。理论上(S_now - S_base)与(T_now - T_base)在经过单位换算后应该大致相等忽略两个时钟的微小漂移。如果某次发现(S_now - S_base)与(T_now - T_base)的差值超过一个阈值例如1秒我们就认为发生了时间跳变。此时我们更新锚点(S_base, T_base) (S_now, T_now)并在日志中插入一条特殊的警告记录。后续的耗时计算应基于steady_clock的差值或者使用跳变后的新锚点重新校准后的system_clock时间。注意steady_clock在系统休眠期间可能也会停止但唤醒后会接着休眠前的时间继续前进不会回退。而system_clock在休眠后可能会被同步到网络时间从而产生跳变。我们的检测方法能有效捕捉这种差异。3. 核心组件实现详解3.1 线程安全的高性能日志队列我们先来实现生产者和消费者之间的通信桥梁。这里采用一个固定容量的环形缓冲区配合互斥锁和条件变量实现一个简单的阻塞队列。选择固定容量是为了防止日志产生速度远大于消费速度时导致内存无限增长。#include queue #include mutex #include condition_variable #include chrono templatetypename T class ThreadSafeQueue { public: explicit ThreadSafeQueue(size_t maxSize) : maxSize_(maxSize) {} // 生产者调用尝试放入一条日志如果队列满则阻塞。 bool push(const T item, std::chrono::milliseconds timeout std::chrono::milliseconds(100)) { std::unique_lockstd::mutex lock(mutex_); // 等待队列非满 if (!notFull_.wait_for(lock, timeout, [this]() { return queue_.size() maxSize_; })) { // 超时放入失败避免生产者无限等待 return false; } queue_.push(item); lock.unlock(); notEmpty_.notify_one(); // 通知一个消费者 return true; } // 消费者调用尝试取出一条日志如果队列空则阻塞。 bool pop(T item, std::chrono::milliseconds timeout std::chrono::milliseconds(100)) { std::unique_lockstd::mutex lock(mutex_); if (!notEmpty_.wait_for(lock, timeout, [this]() { return !queue_.empty(); })) { // 超时队列为空 return false; } item std::move(queue_.front()); queue_.pop(); lock.unlock(); notFull_.notify_one(); // 通知一个生产者 return true; } bool empty() const { std::lock_guardstd::mutex lock(mutex_); return queue_.empty(); } private: mutable std::mutex mutex_; std::condition_variable notEmpty_; std::condition_variable notFull_; std::queueT queue_; size_t maxSize_; };实操要点与避坑超时机制push和pop都设置了超时。这对于生产者和消费者的优雅退出至关重要。想象一下如果消费者线程已经收到停止信号但还在pop上无限等待程序就无法退出。超时后返回false让调用方有机会处理中断逻辑。移动语义在pop中使用了std::move避免了一次不必要的拷贝。我们的日志消息结构体可能包含字符串移动可以提升性能。容量选择maxSize_需要根据实际业务流量权衡。太小容易导致生产者阻塞太大则可能消耗过多内存。通常可以设置为1000到10000条。通知策略在释放锁之后 (lock.unlock()) 再调用notify_one()这是一个好的实践可以立即唤醒等待线程减少不必要的上下文切换。3.2 日志消息结构与双时间戳定义一条日志消息需要包含哪些信息#include string #include chrono struct LogMessage { // 用于外部展示和对齐的壁钟时间可能跳变 std::chrono::system_clock::time_point wallClockTime; // 用于内部排序和间隔计算的单调时间保证单调 std::chrono::steady_clock::time_point steadyClockTime; // 线程ID便于多线程日志追踪 std::thread::id threadId; // 日志等级如INFO, WARN, ERROR std::string level; // 日志正文内容 std::string content; // 源代码位置可选用于调试 std::string sourceFile; int sourceLine; LogMessage(std::chrono::system_clock::time_point wct, std::chrono::steady_clock::time_point sct, std::string lvl, std::string cnt, std::string file , int line 0) : wallClockTime(wct), steadyClockTime(sct), threadId(std::this_thread::get_id()), level(std::move(lvl)), content(std::move(cnt)), sourceFile(std::move(file)), sourceLine(line) {} };为什么需要两个时间戳wallClockTime用于生成人类可读的日志头如[2023-10-27 14:30:01.123456]。steadyClockTime则是我们检测跳变和计算真实耗时的“尺子”。即使wallClockTime因为跳变从14:30变成了13:00steadyClockTime的差值仍然能告诉我们两个日志事件之间真实过去了多少微秒。3.3 时间跳变检测器的实现跳变检测器是一个独立的状态管理类。class TimeJumpDetector { public: TimeJumpDetector() { resetAnchor(); } // 检测当前时间是否发生跳变并返回校准后的“可靠”系统时间 std::chrono::system_clock::time_point checkAndGetStableTime( const std::chrono::system_clock::time_point wallNow, const std::chrono::steady_clock::time_point steadyNow, bool jumpDetected) { std::lock_guardstd::mutex lock(anchorMutex_); jumpDetected false; // 计算两个时钟自锚点以来的流逝时间 auto wallElapsed wallNow - anchorWall_; auto steadyElapsed steadyNow - anchorSteady_; // 将steady_clock的持续时间转换为system_clock的持续时间单位微秒 // 注意这里假设两者精度足够且转换是近似的。更严谨的做法是使用它们的周期比。 auto steadyElapsedUs std::chrono::duration_caststd::chrono::microseconds(steadyElapsed); auto wallElapsedUs std::chrono::duration_caststd::chrono::microseconds(wallElapsed); // 计算差值。由于时钟漂移允许一个阈值例如1秒 const int64_t thresholdUs 1000000; // 1秒 int64_t diffUs std::llabs((wallElapsedUs - steadyElapsedUs).count()); if (diffUs thresholdUs) { // 检测到跳变 jumpDetected true; // 重置锚点到当前时间 anchorWall_ wallNow; anchorSteady_ steadyNow; // 返回新的锚点时间或者直接返回传入的wallNow return wallNow; } // 未发生跳变返回一个基于锚点校准的“稳定”系统时间。 // 这个时间由 anchorWall_ steadyElapsed 计算得出它屏蔽了system_clock的跳变。 // 但注意这会导致输出的系统时间与真实的系统壁钟有偏差。 // 另一种策略是即使跳变也依然返回真实的wallNow但记录跳变事件。 // 这里我们选择返回真实时间并在外部记录跳变。 // 如果需要绝对单调递增的“逻辑时间”可以返回 anchorWall_ steadyElapsed。 return wallNow; } void resetAnchor() { std::lock_guardstd::mutex lock(anchorMutex_); anchorWall_ std::chrono::system_clock::now(); anchorSteady_ std::chrono::steady_clock::now(); } private: std::mutex anchorMutex_; std::chrono::system_clock::time_point anchorWall_; std::chrono::steady_clock::time_point anchorSteady_; };关键逻辑解析锚点初始化在构造时或重置时同时记录system_clock和steady_clock的当前值作为基准 (anchorWall_,anchorSteady_)。差值计算每次检测计算当前时间与锚点时间的差值wallElapsed,steadyElapsed。阈值比较将两个差值都转换为微秒比较它们的绝对差。如果超过阈值如1秒则认为system_clock发生了相对于steady_clock的跳变。跳变处理一旦检测到跳变立即更新锚点为当前时间。这样后续的判断将以新的时间点为基准。同时设置jumpDetected标志让调用者知道发生了一次跳变事件可以记录一条特殊日志如[TIME_JUMP] System clock adjusted by X seconds。重要注意事项这里的阈值1秒是个经验值。你需要根据实际环境调整。在虚拟机或容器中时钟可能不那么稳定阈值可以适当放宽。同时频繁的NTP微调几毫秒到几十毫秒是正常的我们的阈值应该大于这些微调的幅度避免误报。3.4 日志记录器主类设计现在我们把所有组件组装起来形成最终的HighPrecisionLogger类。class HighPrecisionLogger { public: // 初始化日志器启动后台消费者线程 bool init(const std::string logFilePrefix, size_t queueMaxSize 10000) { logFilePrefix_ logFilePrefix; queue_ std::make_uniqueThreadSafeQueueLogMessage(queueMaxSize); running_.store(true); // 启动消费者线程 consumerThread_ std::thread(HighPrecisionLogger::consumerLoop, this); // 初始化时间跳变检测器 detector_.resetAnchor(); return true; } // 停止日志器等待队列清空 void shutdown() { running_.store(false); if (consumerThread_.joinable()) { consumerThread_.join(); } // 最后可能还有残留日志可以在这里选择同步刷新或丢弃 flushRemainingLogs(); } // 生产者接口记录一条日志 void log(const std::string level, const std::string content, const std::string file , int line 0) { if (!running_.load()) return; auto wallNow std::chrono::system_clock::now(); auto steadyNow std::chrono::steady_clock::now(); bool jumpDetected false; // 检测时间跳变 auto stableTime detector_.checkAndGetStableTime(wallNow, steadyNow, jumpDetected); LogMessage msg(stableTime, steadyNow, level, content, file, line); // 如果检测到跳变在原始日志前插入一条跳变警告日志 if (jumpDetected) { int64_t jumpUs std::chrono::duration_caststd::chrono::microseconds(wallNow - stableTime).count(); LogMessage jumpMsg(wallNow, steadyNow, WARN, [TIME_JUMP] System clock jump detected. Offset: std::to_string(jumpUs) us.); // 尝试放入跳变警告日志 if (!queue_-push(jumpMsg, std::chrono::milliseconds(10))) { // 队列满跳变日志可丢弃或输出到标准错误 std::cerr Queue full, time jump log dropped. std::endl; } } // 放入业务日志 if (!queue_-push(msg, std::chrono::milliseconds(10))) { // 队列满处理策略可以丢弃、阻塞、或切换到同步后备路径 // 这里简单输出到标准错误在实际项目中可能需要更复杂的降级策略 std::cerr Log queue full, message dropped: content std::endl; } } // 提供便捷宏方便记录源代码位置 #define LOG_INFO(logger, content) logger.log(INFO, content, __FILE__, __LINE__) #define LOG_WARN(logger, content) logger.log(WARN, content, __FILE__, __LINE__) #define LOG_ERROR(logger, content) logger.log(ERROR, content, __FILE__, __LINE__) private: void consumerLoop() { // 打开日志文件可以使用日期滚动 openLogFile(); std::vectorLogMessage batch; batch.reserve(100); // 批量处理提升I/O效率 while (running_.load() || !queue_-empty()) { batch.clear(); LogMessage msg; // 尝试从队列中批量取出最多100条日志 for (int i 0; i 100; i) { if (queue_-pop(msg, std::chrono::milliseconds(50))) { batch.push_back(std::move(msg)); } else { break; // 队列为空超时退出内层循环 } } if (!batch.empty()) { writeBatchToFile(batch); } else if (!running_.load()) { // 没有日志且运行标志为false退出循环 break; } // 如果batch为空但running_为true说明队列空循环等待 } closeLogFile(); } void openLogFile() { // 根据当前日期生成文件名例如 app_20231027.log auto now std::chrono::system_clock::now(); std::time_t t std::chrono::system_clock::to_time_t(now); std::tm tm *std::localtime(t); char buffer[80]; std::strftime(buffer, sizeof(buffer), %Y%m%d, tm); std::string fileName logFilePrefix_ _ buffer .log; logFile_.open(fileName, std::ios::out | std::ios::app); if (!logFile_.is_open()) { std::cerr Failed to open log file: fileName std::endl; } } void writeBatchToFile(const std::vectorLogMessage batch) { for (const auto msg : batch) { // 格式化时间戳将system_clock::time_point转换为可读字符串 auto t std::chrono::system_clock::to_time_t(msg.wallClockTime); auto tp_us std::chrono::duration_caststd::chrono::microseconds( msg.wallClockTime.time_since_epoch()) % 1000000; std::tm tm *std::localtime(t); char timeBuffer[80]; std::strftime(timeBuffer, sizeof(timeBuffer), %Y-%m-%d %H:%M:%S, tm); // 格式化线程ID std::stringstream ss; ss msg.threadId; std::string threadIdStr ss.str(); // 写入文件 logFile_ [ timeBuffer . std::setfill(0) std::setw(6) tp_us.count() ] [ threadIdStr ] [ msg.level ] msg.content; if (!msg.sourceFile.empty()) { logFile_ ( msg.sourceFile : msg.line ); } logFile_ std::endl; } logFile_.flush(); // 定期刷新平衡性能和数据安全 } void flushRemainingLogs() { std::vectorLogMessage remaining; LogMessage msg; while (queue_-pop(msg, std::chrono::milliseconds(0))) { remaining.push_back(std::move(msg)); } if (!remaining.empty()) { writeBatchToFile(remaining); } } void closeLogFile() { if (logFile_.is_open()) { logFile_.close(); } } private: std::unique_ptrThreadSafeQueueLogMessage queue_; std::atomicbool running_{false}; std::thread consumerThread_; std::ofstream logFile_; std::string logFilePrefix_; TimeJumpDetector detector_; };4. 多线程下的性能优化与问题排查4.1 性能瓶颈分析与优化策略即使采用了生产者-消费者模型在高并发场景下以下环节仍可能成为瓶颈时间戳获取频繁调用std::chrono::steady_clock::now()和system_clock::now()本身有开销。可以考虑在单个日志调用中只调用一次steady_clock::now()而system_clock::now()的调用频率可以适当降低比如每10条日志获取一次然后通过steady_clock的差值推算出一个近似的系统时间。但这会引入微小误差需要权衡。内存分配LogMessage的构造涉及std::string的拷贝或移动。如果日志内容是大字符串开销很大。可以使用线程局部存储TLS的缓冲区或内存池来预分配和复用LogMessage对象或内部的字符串内存。队列争用尽管我们的队列是线程安全的但高并发下锁的争用push/pop时的mutex_会显著增加开销。此时无锁队列如moodycamel::ConcurrentQueue是更好的选择它通过精妙的原子操作避免了互斥锁在多生产者多消费者场景下性能提升明显。文件I/O即使批量写入频繁的flush()也会影响性能。可以设置一个定时器或计数器比如每写入1000条日志或每隔1秒强制flush一次而不是每条日志都flush。但这会牺牲一点数据安全性程序崩溃可能丢失最近未刷新的日志。这是一个典型的 CAP 权衡在性能和数据持久化之间取舍。优化示例使用无锁队列// 假设已安装 moodycamel::ConcurrentQueue #include concurrentqueue.h templatetypename T class LockFreeQueueAdapter { public: explicit LockFreeQueueAdapter(size_t) {} // 无锁队列通常内部管理容量 bool push(const T item, std::chrono::milliseconds /*timeout*/) { // 无锁队列的 try_enqueue 通常很快不需要超时逻辑或者实现更复杂 return queue_.try_enqueue(item); } bool pop(T item, std::chrono::milliseconds /*timeout*/) { return queue_.try_dequeue(item); } // ... 其他接口 private: moodycamel::ConcurrentQueueT queue_; }; // 在 HighPrecisionLogger 初始化时使用 LockFreeQueueAdapter 替代 ThreadSafeQueue4.2 典型问题排查与实战技巧在实际使用中你可能会遇到以下问题问题1日志文件增长过快磁盘空间被占满。排查检查日志级别是否设置过宽比如记录了大量的 DEBUG 日志。检查业务逻辑是否在循环或高频调用中产生了过多日志。解决日志分级实现动态日志级别控制在线上环境关闭 DEBUG 甚至 INFO 级别日志。日志滚动实现按大小滚动如单个文件超过100MB则新建文件和按时间滚动每天或每小时一个新文件并配合日志清理策略只保留最近N天的日志。采样日志对于极其高频的日志点可以采用采样方式比如每1000次请求记录1次。问题2后台消费者线程CPU占用率异常高。排查使用top -Hp [pid]查看线程状态确认是否是日志消费者线程。检查consumerLoop中的循环当队列为空时是否在忙等待busy-waiting。解决我们的实现使用了条件变量wait_for在队列空时会阻塞等待不会忙等待。如果CPU高可能是writeBatchToFile处理太慢或者队列始终有数据导致循环不停。可以增加批量处理的大小减少循环次数或者检查磁盘I/O是否成为瓶颈。问题3程序退出时最后几条日志丢失。排查在shutdown()方法中虽然我们调用了flushRemainingLogs()但如果consumerLoop已经退出而flushRemainingLogs只清空了队列消费者线程可能还没有把最后一批数据写入文件。解决改进关闭序列。shutdown()应该设置running_ false。通知消费者线程可以通过一个额外的条件变量或者向队列发送一个特殊的“毒丸”消息。等待消费者线程完全退出 (join)。此时队列一定为空消费者线程已经处理了所有消息并关闭了文件句柄。这样能保证日志不丢失。问题4时间跳变检测在虚拟机中频繁误报。排查虚拟机的时钟源如kvm-clock可能不如物理机稳定steady_clock和system_clock的漂移可能较大。将阈值如1秒设置得过小。解决根据虚拟化环境调整跳变检测阈值。可以增加到5秒甚至10秒。或者在虚拟机中考虑使用更稳定的时钟源如果宿主机和虚拟机都支持TSC且频率恒定。更激进的做法是在检测到跳变后不立即重置锚点而是观察连续几次的跳变趋势如果是单向的、持续的漂移则可能是时钟本身不准而不是跳变。问题5日志时间戳出现微小乱序同一线程内。排查即使使用steady_clock在多核CPU上不同核心的TSC(Time Stamp Counter) 可能不同步导致获取的时间戳在跨核心调用时出现微小回退。解决这属于硬件层面的问题。对于要求绝对严格单调递增的场景如金融交易订单可以考虑使用一个全局的、原子递增的逻辑序列号来代替时间戳进行排序。时间戳仅作为展示用途。或者将线程绑定到特定的CPU核心但这会影响调度灵活性。下表总结了常见问题与快速排查思路问题现象可能原因排查方向与解决思路程序变慢业务延迟增加日志同步写入阻塞、队列过小导致生产者等待检查队列容量、使用无锁队列、优化消费者I/O批量写、降低日志级别日志文件时间戳不连续或回退系统时间被修改、NTP同步、虚拟机热迁移确认时间跳变检测是否生效检查跳变警告日志调整检测阈值消费者线程CPU占用高忙等待、批量处理逻辑效率低、锁争用检查循环中是否有空转优化格式化字符串操作使用性能分析工具如perf定位热点内存占用持续增长日志产生速度远大于消费速度队列积压检查磁盘I/O是否正常增加消费者线程数实施日志降级丢弃非关键日志程序崩溃或退出时日志丢失关闭序列不完整未等待消费者线程退出实现优雅关闭使用“毒丸”消息确保队列清空且文件句柄关闭5. 扩展与高级特性探讨一个基础的高精度多线程日志器搭建完成后可以根据实际需求进行扩展异步网络日志消费者线程不写本地文件而是将日志批量发送到远端的日志收集服务器如通过UDP、TCP或HTTP。这需要处理网络重连、缓冲、序列化等问题。结构化日志日志内容不再是纯文本而是结构化的数据如JSON、Protobuf。便于后续使用 ELK (Elasticsearch, Logstash, Kibana) 等工具进行解析和检索。在LogMessage中可以用一个std::mapstd::string, std::string或nlohmann::json对象来代替content字符串。日志上下文支持类似 MDC (Mapped Diagnostic Context) 的功能可以为每个线程或每个请求关联一组键值对如request_id,user_id这些上下文信息会自动附加到该线程产生的每一条日志中。这可以通过线程局部存储 (thread_local) 来实现。性能统计在日志器内部埋点统计每秒日志吞吐量、队列平均长度、平均等待时间等指标并通过另一个监控通道上报便于观察日志系统自身的健康状态。配置化将日志级别、输出文件路径、队列大小、滚动策略、跳变检测阈值等参数通过配置文件或环境变量来管理实现运行时动态调整。实现这些高级特性本质上是对核心的生产者-消费者模型和日志消息结构的增强。关键在于保持核心链路的低延迟和高吞吐将可扩展的功能放在消费者线程中完成或者通过额外的、更低优先级的线程/通道来处理。最后关于测试务必编写单元测试来验证时间跳变检测逻辑可以通过模拟修改系统时间但需要管理员权限并进行压力测试用多个线程疯狂写入日志观察内存、CPU和磁盘I/O确保系统在极端情况下依然稳定可控。记住日志系统本身必须是可靠的如果它先于业务系统崩溃那我们就失去了最重要的诊断依据。