2026年2月

今天突然上不去了 gemini ,显示不再支持区域。
但是吊诡的是使用浏览器的隐身可以进入 gemini 页面,但是在隐身模式中登陆账号,gemini 页面又会显示不支持区域。
这个 google 账号的关联区域是美国,支付方式等各种地址都填的美国的地址。

所有看起来 vps 的 ip 并没有标记为中国而是账号被标记为中国?但是之前就是因为更改了关联国家信息才能用的 gemini 和 notobooklm ,为什么会失效呢?

这个账号还有 gemini 的订阅

有没有坛友直到原因和解决的办法?提前谢过了。

使用 chrome 浏览器新建标签
网址使用下面的代码:

javascript:(function(){const showToast=(msg,color)=>{let t=document.createElement('div');t.textContent=msg;t.style.cssText='position:fixed; top:20px; right:20px; background:#333; color:#fff; padding:8px 16px; border-radius:4px; font-size:14px; z-index:999999; opacity:0; transition:opacity 0.3s;';document.body.appendChild(t);setTimeout(()=>t.style.opacity='1',10);setTimeout(()=>{t.style.opacity='0';setTimeout(()=>document.body.removeChild(t),300);},2000);};const copyText=t=>{let n=document.createElement('input');n.setAttribute('value',t);document.body.appendChild(n);n.select();document.execCommand('copy');document.body.removeChild(n);showToast('✅ 颜色已复制: '+t);console.log('颜色已通过传统方式复制:',t);};if('EyeDropper'in window){new window.EyeDropper().open().then(async r=>{let c=r.sRGBHex;try{await navigator.clipboard.writeText(c);showToast('✅ 颜色已复制: '+c);console.log('颜色已复制:',c);}catch(e){console.warn('Clipboard API 失败,尝试传统复制方式');copyText(c);}}).catch(e=>console.log('取色取消或失败:',e));}else{console.warn('浏览器不支持 EyeDropper API ,请使用 Chrome/Edge 95+');}})();

目前来看, 除了大家互相水的帖子外, 比较干的内容大概就三类:

cpp 一直在分享的 ipfs 技术

cnskis Path 一直在分享的安全信息

再就是经常看到的活跃用户们 🐶 分享的一些零散技术或者知识.

看 linux.do 或者 v2ex 他们其实都有很明显调性, 如果是分享产品的话, 去 v2ex 拉新效果很明显比 l 站强, ai 相关的信息去 l 站明显比 v2ex 更全面.

那么 2libra 最后会因为什么来吸引用户呢? 我目前想象不到.

所以我决定以后一周最多水一个帖子, 剩下的都要发点有干货的内容, 内容社区, 从我做起!

在金融级IT系统开发中,处理边缘Case往往比正常业务更考验架构的鲁棒性。最近某美股JMG因审查停牌引发了开发圈的讨论:当一个一直活跃的WebSocket流或长连接突然失去行情数据,且何时恢复未知时,我们的行情组件该如何设计?今天我就从一线架构设计的角度,聊聊如何利用事件驱动模型搞定停复牌场景。

研究痛点:系统在异常边界的脆弱性
传统的拉取式(Pull)系统在面临JMG这类突发停牌时,往往会产生大量无效的空轮询,浪费系统资源;而部分推送式(Push)系统如果设计不当,在长时间没有Tick到来时又容易触发假死。更严峻的是,当监管层突然解除限制,瞬间爆发的交易量和状态切换,极易导致单点应用崩溃,错过核心的交易开端。

数据需求:多态数据的同步聚合
为了让系统平稳过渡并精准捕捉复牌瞬间,我们需要在内存中聚合三种数据形态:

状态机流:实时追踪官方的停/复牌宣告(HALT to RESUME)。

时序切片流:复牌启动后的高频分钟级历史截面数据。

深度快照流:在无撮合状态下的L2/L3盘口挂单薄(Order Book)状况。

落地支持:API层面的解耦集成
在微服务架构下,最好的方案是调用第三方高可用的金融中间件来完成底层数据的清洗。比如引入AllTick API这类兼具深度与事件服务的接口,我们可以很优雅地用几行代码完成原本需要重度解析FIX协议才能做到的事。

以下是Node/Python通用的逻辑骨架表达:

from alltick import Client

client = Client(api_key="你的APIKey")

# 查询股票事件
events = client.market.stock_events(
    symbol="JMG",
    exchange="NYSE"
)

for ev in events:
    print(ev.time, ev.type, ev.description)

状态校验通过后,激活数据抓取模块:

# 获取分钟级行情
candles = client.market.stock_candles(
    symbol="JMG",
    interval="1m",
    start="2026-01-01T09:30:00Z",
    end="2026-02-01T16:00:00Z",
)

for item in candles:
    print(item.time, item.open, item.high, item.low, item.close, item.volume)


风控与策略前置所需的盘口探测:

# 获取盘口数据
order_book = client.market.stock_orderbook(symbol="JMG")
print("买盘深度:", order_book.bids)
print("卖盘深度:", order_book.asks)

实战价值:提升高频监控的系统韧性
将事件状态、分钟价格流和盘口快照通过事件总线串联,不仅提升了系统在极端行情下的抗风险能力,更为上层的算法模块提供了丰富的“开盘前置特征”。对于JMG这种蕴含巨大变数的数据流,能够做到静默期有盘口监控、复牌期有状态感知,你的这套架构在金融级实战中就已经算是及格了。

ManageEngine卓豪 来介绍以ServiceDesk Plus为代表的新一代服务管理平台,通过 CMDB、自动化编排、智能分诊、SLA 管控与可视化治理能力,帮助企业构建“服务韧性架构”,将 IT 服务从被动响应升级为主动防御与持续优化体系。

为什么“服务韧性”成为核心竞争力?

近年来,大规模宕机事件频发。无论是云服务中断、数据库升级失败,还是供应链攻击,一个单点故障都可能引发连锁反应。

服务韧性并不意味着“零故障”,而是:

l 快速检测异常
l 精准识别影响范围
l 缩短恢复时间(MTTR)
l 降低业务损失
l 避免问题再次发生

在传统模式下,服务恢复依赖人工排查,跨团队沟通成本高,数据分散,定位缓慢。而服务韧性架构强调数据整合、流程自动化与持续反馈闭环。

重大事件响应方法论:从“救火”到“体系化战备”

服务韧性架构落地最容易“见效”的地方,就是重大事件响应(Major Incident Response)。 许多组织在重大事件中失利,并不是技术能力不足,而是缺少标准化的响应节奏:谁来判定级别、谁来指挥、如何同步信息、何时升级、何时切换处置策略。

一旦节奏混乱,团队会陷入“多人同时做同一件事”“关键事项无人负责”“业务部门不知道该信谁”的状态,恢复速度被严重拖慢。

真实场景案例:服务韧性架构如何降低停机损失

为了让“服务韧性”不是概念,我们用三个高频行业场景说明它如何落地: 每个案例都包含“触发源—收敛—处置—复盘”的完整链路,以及可量化指标。

连锁零售 POS 异常(门店集中报障)

多门店同时出现支付延迟时,传统模式会产生大量重复工单:每个门店一个工单,技术人员需要逐个阅读、逐个解释、逐个回复。

服务韧性架构的第一动作是“收敛”:系统自动将相似报障聚类并归并为单一重大事件记录,统一公告与进展同步。

l 收敛收益:重复沟通减少、工单处理时间下降,管理层获得统一视图
l 处置策略:启用支付降级方案(备用通道/离线模式),并并行排查上游接口
l 关键指标:MTTA(平均确认时间)下降、MTTR(平均恢复时间)下降、公告发布时效提升

Q1服务韧性架构与 ITIL 有什么关系?
ITIL 提供实践框架,而韧性架构强调把实践工程化落地:流程、角色、数据、自动化与持续改进闭环。 你也可以参考:ITIL 初学者指南。

Q2没有 CMDB 还能做韧性吗?
可以先从事件与变更闭环做起,但 CMDB 能显著提升影响评估与根因定位效率。 可延伸了解:什么是 ITSM。

Q3重大事件是不是一定要开大会?
不一定。关键是统一节奏与指挥链。轻量事件可通过标准流程与公告模板快速推进,只有 P1/P2 才需要更强协同。

Q4自动化会不会带来更大风险?
自动化必须分级:先做低风险规则(通知/分派/模板),再做可回滚的执行动作,并保留审计与审批机制。

Q5如何评估投入产出(ROI)?
重点看停机损失下降、重复劳动减少、变更失败率降低、以及员工体验提升(满意度与等待时间)。这些都能通过报表持续量化。

1. 背景与趋势

2018 年,我提出过测试计划驱动开发(TPDD),试图以一种比 TDD 更友好的方式组织开发流程。八年过去,技术世界已经发生深刻变化。

大模型正在显著加速软件开发范式的演进。以 ChatGPT 等为代表的生成式 AI,已在实际工程中明显提升开发效率,许多过去需要人工完成的编码与调试工作,正在被模型部分甚至大规模接管。

与此同时,自然语言直接生成程序的能力也在快速成熟。随着 Agent、Skill 等技术与新概念的出现,少量程序员,甚至非科班人员,仅凭自然语言描述,就能够生成可运行程序。这种从“写代码”到“描述意图”的转变,正在重塑软件生产的基本形态。

更关键的是,这一能力并未停留在玩具级 Demo。经过多轮迭代与工程化实践,以自然语言为入口、以智能体为执行核心的开发方式,已经开始具备支撑 SaaS 级应用的现实能力。进入 2025 年,这一趋势进一步加速。Agent 与 Skill 体系逐步成型,使开发者可以在数月、数周,乃至数小时的连续迭代中,生成结构完整的大型项目。从原型验证到功能闭环,再到初步可用的产品形态,AI 正在实质性压缩软件生产周期,并已在部分场景中支撑 SaaS 级应用开发。

到了 2026 年,AI 的爆发呈现出明显的“破圈”特征。个人智能体(Personal Agent)的推出,彻底点燃了公众关注度。Agent 不再只是从业者圈内的效率工具,而是开始走向大众使用场景;它也不再停留在实验室或概念验证阶段,而是真正迈入普及化工具时代。

在这一阶段,个人或小团队可以通过 AI 完成代码生成、应用部署乃至运营管理,但与此同时,风险与责任也出现高度集中。

换句话说,Agent 正在完成一次关键跃迁: 研究原型 → 工程工具 → 公众基础设施

这一步跨越,意味着软件生产方式与人机协作结构,正在进入新的历史阶段。

2. 核心问题

随着 AI 深度介入开发过程,一种新的生产路径正在形成:想法 → 说出 → 描述 → 生成

自然语言第一次成为“可执行接口”。开发流程被极度压缩,但与此同时,系统内部逻辑的可见性正从白盒逐渐滑向灰盒,甚至黑盒;越是如此,对 AI 的依赖反而越高。

随着对 AI 依赖的不断加深,代码的可控性却在相对下降,不可控性与潜在风险同步放大。(增强说明:例如使用第三方 Skill 或 Agent 时,如果未进行沙盒化测试,可能导致敏感数据泄露或系统破坏)

基于大量实践观察,我将当前主要风险来源归纳为三点:

  • 使用 AI 的策略失衡

  • 团队 QA 能力与质量意识缺位

  • 组织文化与流程发生冲突

在 OPC 或单人团队模式下,这三类风险往往集中到个人身上,意味着个人需要同时承担开发、运维、安全、合规与财务责任。

2.1 能力暴露与禁止空间的结构性失衡

在大量 AI 使用实践中,一个被普遍忽视的事实逐渐浮现:多数团队专注于“还能让 AI 做什么”,却缺乏“AI 绝对不能做什么”的系统性思维。

这类似于操作一台功能极其强大的自动化系统,却没有设置安全边界。再先进的自动驾驶汽车,也必须配备方向盘、安全带与安全气囊;AI 同样需要明确的限制与保护机制。

在 AI 工程中,理解模型“能做什么”和“不能做什么”同等重要:

  • 能力暴露(Capability Exposure):定义能力边界

  • 禁止空间(Forbidden Zone):定义安全边界

二者本应是一体两面,但现实中绝大多数团队严重偏向前者。

许多使用者只关心:用了什么 Skill、搭了什么 workflow、选了哪个 model,却很少关注系统验证与边界控制,甚至缺乏基础测试,更多依赖“感觉不对就让 AI 再改”的试错模式,更遑论系统级红线设计。

能力暴露(Capability Exposure)

Skill 的本质,是对 AI “能做什么”的结构化表达。例如:

  • 功能:生成报表

  • 输入:CSV 数据

  • 输出:PDF 文件

  • 调用方式:agent.run("生成报表", data)

当前主流心智模式是: 我要实现什么 → 让 AI 做更多 → 不断补丁式修正,这种模式在能力扩展上极其高效,但在约束缺失时,也极易放大系统风险。

禁止空间(Redline / Forbidden Zone)

相比之下,“不能做什么”往往被系统性忽视。许多 AI 系统没有明确的禁止空间定义,只在出问题后被动修补,例如 OpenClaw 不推荐在主力机上使用,因为存在大量安全问题。(增强说明:建议所有第三方 Skill / Agent 都在沙盒或备用机环境中运行)

典型风险包括但不限于:

  • Prompt injection 导致敏感信息泄露

  • 自动删除或覆盖本地文件

  • 越权访问企业知识库

  • 浏览网页后向外发送内容

  • 对 XSS 或提示注入缺乏防御

没有防御性规范的 AI 系统,本质上是高能力、低护甲的高风险体。

值得注意的是,TPDD 的高层测试计划天然可以缓解这一问题:通过提前定义测试点与边界条件,使 AI 在执行阶段始终处于可约束空间内,从而在能力利用与风险控制之间取得平衡。(增强说明:可以通过 TestPlan.md 定义每个 Skill 的边界、禁止操作、异常路径触发条件)

2.2 QA 思维的系统性缺位

即便部分 AI 使用者具备基本验收意识,对产品质量的整体把控仍普遍不足。常见状态就是:功能能跑通 → Happy path 通过 → 即视为完成。

这种模式在传统开发时代就已存在,而在 AI 时代被进一步放大,而更关键的是:AI 使用者本身,正在天然转变为质量把控者,仅做表层测试已远远不够。

Failure Thinking 是保证 AI 自动化生成代码可控、可维护的核心手段。 使用者必须同时具备:代码阅读与审计能力、需求一致性判断能力、系统风险识别能力。

这意味着,每一个 AI 使用者都需要具备一定程度的抗风险 mindset——以批判性与怀疑性视角审视系统,以安全与需求为底线,持续评估系统的稳定性、正确性与安全性,而 QA 的核心能力,恰恰天然契合 AI 安全落地需求,包括但不限于:

  • 边界条件思维

  • 异常路径覆盖

  • 非法状态识别

  • 失败模式分析

  • 防御性设计意识

相比之下,单纯开发导向的实践,往往系统性缺乏 Failure Thinking 与禁止空间意识。

因此,在 AI 深度参与的软件体系中:

  • QA 不再只是角色,而是一种必须被团队共享的工程心智

  • TPDD 的价值之一,正是在流程层面让不同角色、不同背景的人都能参与到测试计划与约束设计中,形成多视角监督,从而共同承担 AI 的安全性与功能正确性责任

  • 在这个过程的同时,不仅是开发人员,所有的参与者将会一起提升

通过引入 TPDD,所有 AI 使用者——不仅仅是 QA——都会参与测试计划的设计和边界定义,从而直接或间接地掌握基本的测试知识。更重要的是,这种做法将 QA 思维方式扩散到整个团队。

2.3 团队分化与文化的应对方式

当前行业对 AI 的使用正在出现明显分化。不同团队与个人,在“是否采用”“采用深度”以及“嵌入流程位置”上,逐渐收敛为两种典型路径:

两者的本质不在于是否使用 AI,而在于:是否为 AI 建立了足够强的工程约束体系。高产型团队往往把 AI 当作生产力放大器,却低估了不确定性扩散的速度;而克制型团队更早意识到,大模型在提升生成效率的同时,也同步放大了系统状态空间与潜在失效面,因此会主动构建结构性护栏。

可以预见,随着 Agent 从实验室工具走向公众基础设施,真正能够穿越周期的,不会是“生成速度最快”的团队,而是那些最早完成 AI 工程化闭环设计的团队。而这,正是 TPDD 与高层测试闭环方法论试图系统回答的问题。

3. TPDD 在 AI 开发时代的价值

TPDD(Test Plan Driven Development) 的核心思想是:用测试计划驱动设计。在这种模式下,开发者会参与测试计划的制定,而后再开展开发,同时包括 BA、PM 和 QA,从而明确系统需求、优先级以及边界条件。

在讨论 TPDD 之前,不可避免要提 TDD(Test Driven Development)。TDD 在传统软件开发中非常重要,其核心目标是确保代码质量和可测试性。但在 AI 开发时代,TDD 流程逐渐被 AI 内化

  • 单元测试和代码生成自动化,由模型直接完成

  • 细粒度验证被 AI 自我校验替代

  • 开发者不再直接参与每次单元级循环

也就是说,TDD 的价值依然存在,但执行路径已经被 AI 替代,就像我们不必亲自浇水种土豆一样。未来,单元测试可能完全由 AI 内化,而再上一层,测试计划将成为控制 AI 行为、检测功能是否符合预期的最后防线。

AI 场景下的核心问题

随着 AI 内化单元验证,工程治理重心发生迁移。团队必须回答三个关键问题:

  1. 功能应该是什么

    边界在哪里

  2. 什么叫真正的“通过”

TPDD 策略是:先定义系统必须满足的条件,再允许 AI 去实现,同时始终保持人类对边界的控制和验证。

TDD 与 TPDD 的对比

相比 TDD 注重细粒度单元测试,TPDD 更关注高层测试计划:先定义核心检查点、必须通过项和优先级,再回到代码开发与测试细化阶段。

核心组成

  • 开发承诺(Development Promise) 众多角色(开发、测试、BA、PM 等)共同制定功能检查点和系统边界

  • 测试计划 A / B

  • A: 开发者提出功能构想及检查点划分

  • B: 测试人员在 A 的基础上细化为可执行测试策略与用例

优势:提前明确预期行为、优先级、测试覆盖范围,并形成不可造假约束。 在 AI 场景下,skill.md 定义 AI 功能指令,而 TestPlan.md 定义功能检查点或系统边界。

TPDD 的传统应用场景模式

在 TPDD 中,开发者协助测试人员起草 测试计划 A(开发承诺 / Development Promise),再进行实际开发。

开发承诺 / 测试计划 A 的结构

  • Must Have – Critical Check Points

  • 必须完成的功能点,未完成视为工作未完成

  • 测试重点:功能验证 + adhoc 测试

  • 高能力团队可纳入自动化测试

  • Need Have – Important Check Points

  • 重要功能点,必须完成大部分

  • 测试重点:功能验证

  • Should Have – Optional Check Points

  • 可选功能,由开发者决定实现与否

  • 测试重点:手动测试

作用

  1. 明确优先级,提供开发与测试指导

  • 对开发者:理清任务优先级,快速理解任务,即使团队成员离职或请假,其他开发者也能迅速开展工作

  • 对测试人员:明确重点测试、实验性测试以及需自动化的功能,提高测试效率并顺利整合 CI/CD

  1. 促进开发者谨慎编码,提高质量

  • 利用心理学“承诺效应”,开发者会自然约束行为,确保开发过程与承诺一致

  • 知道至少要通过哪些测试,从而提升代码可靠性和整体质量

  1. 优化测试人员工作流程

  • 测试人员可在测试计划 A 基础上起草 测试计划 B,明确具体测试方法

  • 为自动化测试、adhoc 测试及运维工作争取更多时间

4. TPDD 的 AI 场景应用

 

心理学参考

一旦做出承诺或选择立场,个人会在内外压力下倾向于保持言行与承诺一致,即使可能与自身意愿相悖。

正是这一心理学效应,使 TPDD 的开发承诺机制能够有效提升团队执行力、代码质量和系统可靠性。

随着 AI 的逐渐强大,自动化生成代码和测试用例已成为可能。曾经需要程序员手动思考的“怎么开发”,在今天已经被 AI 内化——代码生成、测试验证、循环迭代,都能自动完成。那么,我们还能做什么?答案是:设计、调整和规范测试。也正因为如此,TPDD 再次成为主流,其价值被重新凸显。

九年前,我所提出的,TPDD 只是“测试计划先行”的理念,强调团队共识、边界定义和高层控制。而在今天,AI 可以自动生成代码和测试用例,这种“高层先行 + 验证闭环”模式,天然契合 AI 的执行逻辑。开发者不再需要手动验证每条单元测试,AI 已经内化了 TDD 的精细循环;而 TPDD 的高层规划则提供了宏观控制、边界约束和失败模式定义——正是 AI 最缺乏的能力。

TPDD 天然解决了 AI 的最大痛点。AI 可以生成功能,但缺乏边界意识、Failure Thinking 和禁止空间。TPDD 早就把这些概念融入流程:明确边界、优先级划分、必须通过的核心检查点……这些正是 AI 在自动化执行时所急需的安全约束和指导。

换句话说,TDD 被 AI 内化执行,TPDD 则提供了宏观控制和安全保障。高层测试计划 + 核心检查点,在 AI 场景下成为天然适配的框架,让开发不仅高效,而且可控、安全、可持续。

 

保留专门作用于能力暴露(Capability Exposure)的 Skill.md 所擅长的能力,并将其与 TPDD 结合,相当于形成一个安全与质量的 Skill.md

在实践中,可以使用一个类似 Skill.md 的 AI 指示文件,例如 TestPlan.md,或者任何其他命名方式。其最核心的目的只有两件事:TestPlan.md = 内部限制 + 外部验证

  1. 内部限制:定义 AI 可做与不可做的操作边界,将禁止空间显式化,防止越界和潜在风险。

  2. 外部验证:提供可执行的高层测试规范,使 AI 在生成代码或执行操作时,能够自动参考并遵循这些边界与检查点,实现持续验证与安全保障。

 

内部限制 禁止空间(Redline Definition)

  • 内部限制

  • 明确禁止操作、非法组合、不可接受副作用

  • 建立系统边界与安全沙盒

外部验证 验证计划 (Verification Plan)

  • 外部验证核心:可观测验证系统是否按预期运行

  • Must Have:核心功能,必须通过

  • Need Have:次要功能,必要验证

  • Should Have:可选或附加功能

工具映射:

  • TestPlan.md ← 测试约束

  • skill.md ← 能力暴露

  • redline.md ← 禁止空间 (也可以将内部限制和验证分开存放)

5. TestPlan.md 示例:AI 生成销售报表

功能描述 (Capability)

  • 功能名称:生成销售报表

  • 输入:CSV 格式销售数据(包含日期、产品、数量、单价、地区等字段)

  • 输出:PDF 或 Excel 报表

  • 调用方式:agent.run("生成销售报表", data)

  • 可选参数:报表模板、汇总方式(按产品/按地区)、日期范围

能力边界 (Boundary)

禁止空间 (Forbidden Zone / Redline)

  • 不允许访问非报表相关本地文件

  • 不允许上传或发送敏感客户数据到外部服务

  • 不允许删除原始 CSV 文件

  • 不允许调用未经授权的第三方 API

  • 不允许执行任意代码(防止 RCE)

测试策略 (Validation Plan)

Must Have 核心功能

  1. 输入有效 CSV,生成 PDF 报表

  2. 报表内容正确,包括汇总、统计和图表

    调用超过 50 次触发限制时返回错误提示

Need Have 次要功能

  1. 支持不同报表模板

  2. 支持按日期或地区筛选数据

  3. 支持 Excel 格式输出

Should Have 可选功能

  1. 支持自动邮件发送报表

  2. 支持报表加密(可选密码保护)

  3. 支持自定义字体和颜色(仅内部批准模板)

异常路径 (Failure & Edge Cases)

审计与日志

  • 每次生成报表必须记录调用用户、时间、输入数据摘要、输出文件名

  • 异常触发必须记录完整日志,并告警到管理员

Token / 安全约束

  • 每次生成报表消耗 1 Token

  • 当 Token 剩余 < 10 时,触发提醒

  • 沙盒环境执行,避免主机或生产数据库风险

这个 TestPlan.md 可以直接作为 TPDD 的高层测试计划使用,同时配合 skill.md 描述 AI 能力、redline.md 定义禁止操作,就形成完整闭环

6. CTO 行动建议

  • 保留 TDD,但不再是最高控制层

  • 强制 AI 功能配套 TestPlan.md

  • Must / Need / Should 分级验证

  • 外部验证优先

  • 第三方 Agent / Skill 沙盒运行

  • Token 预算护栏

  • 渐进式生成代码

系统可靠性取决于不可绕过的测试计划,而非单元测试覆盖。

7. 长远价值与总结

TDD 的内化,使 TPDD 高层测试计划成为 AI 开发闭环的天然工具。

Development Promise + Test Plan A/B + Verification Plan 将共同构建可控、可维护、可扩展的软件系统。

在 OPC 或个人开发模式下,安全、沙盒化与 Token 控制已成为必选项。

TPDD 不仅是一套流程,更是一种工程心智,用于确保团队或个人在 AI 高产环境中仍能牢牢掌握系统可靠性。

最终结论:在 AI 开发时代,能力暴露与禁止空间必须并行;速度不再是第一目标,可控、可维护、可扩展,才是能够穿越周期的长期价值。

 

“Text polished by GPT; images generated by AI.”

前段时间公司用的鼠标坏了,遂换了个 MX Master 3s,今天到手适应了一下……
然后发现移动鼠标的时候,能明显感觉到底部有种颗粒感,想着是不是鼠标垫脏了,就擦了又擦,还是感觉不顺。
索性直接撤了鼠标垫,在木质的桌面直接用了,没想到丝般顺滑……

中午上网搜了下鼠标垫推荐,发现这玩意居然也有一堆概念名词:中性手感,搓澡巾手感,绒布手感,玻璃板……再看看公司里那张黑灰大饼鼠标垫,给我整不会了。

那么问题来了,究竟是不是鼠标垫的问题?各位平时用啥鼠标垫啊,v 站 Master3s 的用户应该也不少?

一、格局已变:工业AI不再是实验室里的玩具
人工智能的浪潮已经席卷各行各业,而工业AI作为这场革命的核心战场之一,其竞争态势正发生深刻变化。以往,工业领域的智能化更多依赖于传统工业巨头的经验和专长,信息技术公司则提供底层工具。如今,随着数据量的爆炸式增长、计算能力的指数级提升以及算法模型的日益精进,AI正以前所未有的方式介入工业流程的核心环节,从设计研发、生产制造、质量控制到供应链管理、设备维护,无一不被其触角所覆盖。这种深度融合催生了全新的工业竞争格局,不再是简单的“谁制造得更快”或“谁维护得更省”,而是演变成一场关于效率、成本、创新和可持续性的全方位竞赛。
二、中国登场:不是简单的跟随者,而是独特的推动者
在这场全球工业AI的竞争中,中国企业扮演的角色日益凸显其关键性。然而,与许多人的预期不同,中国并非仅仅是技术追赶者或市场跟随者,而是在以一种独特的方式——即输出“中国方案”——影响着竞争格局的走向。
首先,中国的制造业体量为工业AI的应用和创新提供了肥沃的土壤。作为全球最大的制造业国家,中国拥有从低端组装到高端装备制造的全产业链,这使得中国企业在各种规模和类型的工业场景中积累了海量的运营数据和经验。这些真实的、复杂的工业环境需求,反过来又驱动了AI技术的快速迭代和场景化解决方案的落地。
其次,中国在工业AI领域的突围路径具有鲜明特色。不同于西方科技巨头主导的“从上到下”的技术路线,中国企业更倾向于走“从下到上”的路径,即先深耕具体的垂直领域和应用场景,再逐步构建技术壁垒。这种模式下,技术的开放性和实用性往往被置于优先位置,很多中国企业选择开源策略,以降低技术门槛,快速建立生态。DeepSeek等公司的实践,展示了低成本、高效率的开源AI方案如何在全球范围内引发关注和应用,这本身就是对中国方案的一种肯定。
最后,中国政府的战略引导和政策支持对工业AI的发展起到了强大的推动作用。从产业规划到专项扶持,一系列政策旨在加速制造业的数字化转型,并鼓励企业在AI领域进行自主创新。这种自上而下的推动力,加上庞大的国内市场所带来的用户规模和数据量优势,为本土工业AI企业提供了独特的成长环境。中国正在努力构建一个开放、协同、共享的工业AI生态系统,这本身就是一种具有全球影响力的“中国方案”。
三、案例聚焦:中国领跑,国际巨头呼应
为了更直观地理解中国方案在全球工业AI竞争中的作用,我们来看几个具体的案例。

  1. 广域铭岛:本土工业AI的标杆与实践者
    广域铭岛作为国内工业AI领域的领军企业,其成功在很大程度上印证了中国方案的有效性。该公司基于自身在汽车制造(尤其与吉利集团的深度合作)及其他行业的经验,开发了Geega系列工业软件和平台。其核心理念是“工业知识AI化”,即将企业的工艺诀窍、管理经验转化为可计算、可迭代的AI模型。
    在领克汽车成都工厂的实际应用中,Geega平台的应用带来了显著效益:焊接过程中的质量损失成本降低了13%,订单交付周期缩短了15%。此外,在电解铝等传统重工业领域,该平台同样展示了其优化能源消耗、提升生产效率的能力。这些成果不仅体现了中国工业AI技术的成熟度,更重要的是展示了其在特定场景下的巨大商业价值,为中国方案赢得了国际市场的认可。
  2. PTC:工业数据与AI的深度融合
    PTC是工业AI领域国际巨头的代表之一,其ThingWorx工业物联网平台强调的是将AI能力深度集成到工业数据流中。PTC的核心优势在于强大的设备数据连接和管理能力,以及在其平台之上构建的、针对工业特定需求的AI应用生态。
    3. C3.ai:企业级AI平台的全球布局
    C3.ai是另一个值得关注的国际工业AI平台型公司。它定位于为企业(尤其是大型制造和能源企业)提供一个集成的数据和模型管理环境,支持跨业务领域的AI应用开发。

中午就靠这么一杯 manner 撑着了,以前一直点外卖,公司附近 900 米的 manner,配送 9 分钟,备注要奶泡之后都会给

打后奶泡的,结果,这两天,就是不给打!!说是公司规定,明明之前几个月都可以的!

好气人阿!没有奶泡的 manner 咖啡,还不如喝楼下的瑞星,娘的,郁闷!!

ManageEngine卓豪 来介绍什么是IT 运营财务化!

IT 运营财务化并不意味着简单削减成本,而是将 IT 服务能力与财务语言对齐:让每一次事件处理、每一次变更上线、每一次资产更新,都可以回答三个问题:

l 这项服务的真实成本是多少?
l 它为业务创造了多少价值?
l 如果中断或失败,损失是多少?

当 IT 能够用“财务可理解的语言”表达自身能力时,IT 便从被动支持转向主动决策参与。

为什么 IT 必须进入“财务语言时代”

过去十年,企业数字化程度大幅提升。线上交易、数据驱动决策、云原生架构、AI 应用平台……几乎所有业务增长都依赖 IT 基础设施。 但与此同时,CFO 对 IT 预算的压力也在持续增强:

l 云支出增长不可控
l 软件授权重复采购
l 资产利用率不透明
l IT 项目 ROI 难以证明

问题并不在于 IT 花钱多,而在于 IT 无法用“价值视角”解释花钱的合理性。

当 IT 无法清晰呈现资产生命周期成本(TCO)、服务级别协议违约成本、重大事件停机损失估算时,预算讨论就会退化为“削减比例谈判”。

IT 运营财务化的核心目标,是让 IT 服务具备以下三种能力:

l 成本透明:服务成本可追溯至资产、人员与时间投入
l 价值量化:服务收益可关联业务指标与用户体验
l 风险定价:中断风险可转换为财务损失模型
IT 成本结构重构:从“设备清单”到“服务单元”

传统成本模型围绕“设备”展开:服务器多少钱、软件授权多少钱、人员工资多少。 但业务并不关心“服务器价格”,而关心“支付系统是否稳定”“CRM 是否可用”。

因此,IT 成本必须从“资产导向”转为“服务导向”。

SLA 经济学:服务级别协议背后的成本与价值逻辑
在多数组织中,SLA(服务级别协议)往往被视为“技术指标”:响应时间、解决时间、可用性百分比。 然而在财务视角下,SLA 本质上是一个经济模型。

每一个 SLA 承诺,都隐含着资源投入与风险成本。如果企业承诺“99.99% 可用性”,那么它必须承担更高的基础设施冗余、值班人员成本与监控系统投资。

SLA 经济学的核心在于:可用性提升的每一个小数点,都伴随着成本指数级上升。

l 99% → 99.9%:增加监控与备份
l 99.9% → 99.99%:增加双活架构与容灾系统
l 99.99% → 99.999%:增加跨区域冗余与自动故障切换

行业案例:IT 财务化如何驱动业务决策

案例 1:零售企业的“服务定价模型”

某零售集团将 IT 服务拆分为“门店支持单元”,计算单门店 IT 成本。 结果发现 30% 成本来自低利用率服务器。 通过整合架构,年度节省 800 万元。

案例 2:制造业的“停机损失测算”

通过建立停机损失模型,IT 成功说服管理层投资双活系统。 投资 1200 万元,但预计 3 年内避免停机损失 5000 万元。

案例 3:金融行业的“合规成本可视化”

通过审计日志与风险模型,量化违规风险成本,避免重大罚款。

Q1:IT 财务化是否意味着 IT 要盈利?
并非如此,其核心是透明化与价值证明。

Q2:是否所有企业都适合成本分摊?
小型企业可以采用 Showback 机制,而非直接收费。

Q3:如何快速开始 IT 财务化?
从数据整合与报表分析入手,建立基础指标体系。

Q4:是否需要更换 ITSM 系统?
若现有系统支持报表、SLA、资产管理与自动化能力,则可逐步升级。

之前一直用的好好的,今天早上软件自动更新后,发送命令后没有输出,一直显示 Working... Loading... Generating... 三个循环。
另一台电脑一开始没更新软件,能正常用,软件关闭后就自动更新了,再打开后也和上面一样的结果。
这是啥原因,账号被封了嘛? 没有用过反代,显示的配置剩余也是正常的,网页版也能正常用。

ManageEngine卓豪来介绍什么是“数字韧性运营”!

在企业全面数字化之后,IT 已经不再只是支撑系统运行的后台部门,而是业务连续性、客户体验与品牌信誉的核心保障。 这意味着组织必须构建一种能够抵御冲击、快速恢复并持续优化的运营体系。

这种体系的基础,往往建立在成熟的 IT 服务管理、 标准化的 ITIL流程, 以及统一的 ServiceDesk Plus 平台能力之上。

数字韧性运营的目标,并不是让系统永不出错,而是确保: 出现异常时能够被迅速发现; 影响范围被快速评估; 处置流程被自动触发; 沟通机制保持透明; 经验被沉淀为可复用的知识。

传统 IT 运维模式为何缺乏韧性?

在很多企业中,运维仍然以“人工响应 + 经验判断”为主。 当异常发生时,团队通过邮件、微信群、临时会议进行协调, 直到有人定位到根因,才逐步恢复。 这种模式的核心问题在于:高度依赖个人经验,缺乏系统化编排。

l 故障定位依赖个人知识
l 跨部门沟通缺乏统一平台
l 问题记录与知识沉淀脱节
l 风险无法提前预测

抗冲击架构模型:从“响应型”走向“预防型”

数字韧性运营的核心不在于“故障处理速度”, 而在于构建一套能够识别冲击、缓冲冲击、恢复冲击并持续优化的服务架构。 我们可以将其拆解为四层模型:

l 监测层:实时收集系统与业务信号
l 判断层:基于规则或模型评估影响范围
l 执行层:自动触发标准化处置流程
l 复盘层:沉淀知识并优化规则

在传统模式下,这四个步骤往往由不同团队分别完成, 且缺乏统一视图。结果就是响应时间长、沟通成本高、重复劳动严重。

Q1:数字韧性是否等同于灾备?
不是。灾备是恢复能力的一部分, 而数字韧性包括预防、检测、响应与优化全周期。

Q2:中型企业是否需要完整韧性模型?
建议从关键流程开始逐步推进, 不必一次性覆盖全部场景。

Q3:如何衡量投资回报?
可通过减少停机时间、提升满意度、 降低返工率进行量化。

Q4:是否必须引入 AI 才能实现韧性?
AI 可以加速成熟度提升, 但基础流程治理同样重要。

如题,APP2 不用基本上也不怎么耗电,但是这新买的 APP3 ,一个周我基本上只用不到三小时,居然还是会跑电,我一开始觉得是不是耳机在口袋里塞着,盖子不小心打开了,最近正好过年,基本上都在桌子上放着,结果也这样。

不知道谁碰到过这种情况?

你有没有发现一个特别有意思的现象:一个人越是侃侃而谈、笃定乐观,说起话来毫无保留、气势十足,往往越不是他最擅长的领域;反倒是那些聊到某个话题,突然语速放慢、用词谨慎,加了一堆定语限制、预设一堆前提条件的人,才是真的懂行。

640

一、外行敢吹,内行谨慎

外行看世界,从来都是非黑即白的。懂一点皮毛的人,最容易自信爆棚,因为他只看到了冰山一角,没见过水下的暗礁、暗流,没了解过一件事背后的复杂逻辑和诸多限制,所以敢随口下结论、敢打包票、敢把话说得绝对。

就像酒桌上聊行业趋势,那些半懂不懂的人,总能高谈阔论、指点江山,从宏观环境说到未来走向,语气里满是笃定,仿佛一切都在掌控之中;聊到某个具体的工作项目、问题卡点,他们总是用「这很容易」的口头禅一笔带过,就好像这个世界如同「1+1=2」那样简单,只要输入你的要求就一定能得到想要的结果。

而真正扎进一个领域的专家,恰恰相反。他们太清楚一件事是怎么做出来的,太知道中间有多少妥协、多少运气、多少不完美,太明白任何结论都是有边界的、有条件的、有例外的。我们做的大多数努力,都是为了更科学的决策,都是为了提高一些成功的概率,但从不能确定结果一定 100% 符合预期。

所以你看,聊宏观、聊趋势、聊别人家的事,他们可以谈笑风生、逻辑清晰;可一说到自己深耕多年、了如指掌的细分领域,反而变得「没底」了,不敢说绝对,不敢说完美,不敢打包票,甚至会先否定掉一大半可能性,才小心翼翼给出一个模糊的、留有余地的谨慎判断。

这不是不自信,而是深耕后的敬畏。正如古希腊哲学家芝诺提出的「知识圆圈」理论:人的已知领域像一个圆圈,圆圈越大,圆周(即已知的知识边缘)与未知区域的接触面也就越大。真正的博学者因为充分认识到世界的复杂性而清楚自己的渺小和局限,知识匮乏者反而因接触面小而觉得自己无所不知。

640 (1)

二、内行的误区:自我贬低

真正的内行,还有一个很常见的困扰 —— 容易陷入「自我贬低」的误区。

因为知根知底,他们太清楚自己手里的东西有多少瑕疵、多少妥协、多少不完美。对外宣传时,哪怕只是适度放大优势,自己心里都虚得不行,总忍不住吐槽「我们也就那样,没那么厉害」。

这种自我贬低,源于对专业的极致较真,却也常常让他们陷入认知偏差 —— 忘了一句大实话:世界本来就是个草台班子

你以为自己是草台班子,其实大家都是。你觉得自己的东西没那么好,别人的也一样,区别只在于「吹牛逼」的程度不同。

我们习惯老实一点,吹牛逼顶多翻个倍,心里还会反复打鼓,总觉得要留有余地;可某些「草台班子」,更擅长包装、更敢讲故事,他们吹牛逼从不是翻倍,而是直接加一个 0、两个 0 起步。外面看起来光鲜亮丽、无可挑剔,可拆开内里,一样是漏洞百出、混乱不堪。

这种现象,在 科技、军事、产业对标 里尤其明显。

每次新闻里说,我们在某个领域突破、赶超国际先进水平,普通群众一片欢呼、热血沸腾;可真正在一线的行业专家,反而冷静、沉默,甚至有点「不屑一顾」。不是他们不爱国,而是他们太懂内部细节:知道宣传里有包装、有取舍、有美化,真实水平并没有文字里那么完美。你跟他们深聊,他们常常会叹气、会心虚,最后来一句:「唉,其实真没那么牛逼」。但这里面,藏着一个巨大的认知误区:我们真心以为,自己对标的是别人的真实水平。

最近两年的「中美全方位 PK」以及轰轰烈烈的「民间大对账」下来,我们才慢慢看清真相:美国吹的牛,比我们大得多。

我们对标追赶的,根本不是他们的真实能力,而是被宣传放大了十倍、甚至永远停留在 PPT 上的「神话版本」。他们很多概念、装备、技术,画饼一流,落地稀碎;我们却信以为真,拿它当目标死磕、拼命追赶,硬生生把东西做了出来,做完还在自我怀疑:「我们是不是还差很远?」

真实情况往往是:我们早就超过那个「真实的他们」了,只是一直在跟那个「吹出来的他们」较劲。而我们自己,只是稍微包装一下、翻个倍宣传,就已经心虚不已;别人是直接加零、无限画饼,却被全世界当成标准答案。

640 (2)

三、我们需要「清醒的谨慎」

说到底,外行和内行的本质区别,从来不是「懂多少」,而是「怎么看待自己懂的东西」。

无知者无畏,懂的越少,越自信越张扬;懂的越多,越敬畏越谨慎。

外行只看表面,越看越自信,因为他们看不到背后的复杂与局限;内行看透本质,越看越谨慎,因为他们深知每个结论、每个成果,都有其边界和不足。

以后再看人说话,不用只听他有多笃定、多豪迈。真正值得你信任的,从来不是那个什么都敢说、什么都懂的人,而是那个聊到专业领域,突然变得犹豫、谨慎、反复限定条件,敢说「不确定」,坦诚自己「其实没那么有底」的人。

真正的高手,从不会用绝对的语气彰显自己的专业,反而会用谨慎的态度,守住专业的底线。他们承认自己的「没底」,不是不自信,而是见过足够多的未知,懂得敬畏专业、敬畏规律;他们偶尔自我贬低,不是真的不行,而是对自己有更高的要求。

我们或许都是这个世界上,认真运转的「草台班子」,我们的成果或许不够完美,我们的能力或许有局限,但这份「知根知底的谨慎」和「不夸大其词的诚实」,恰恰是最难得的清醒,也是最硬的底气。

1. !!厂家必须与“极客湾视频下架事件”无关!!
任何有可能与该事件有利益关系的厂家均不考虑,除非他发表声明说不是他家干的,在此之前假定所有相关厂家都有问题。

2. 无须考虑游戏性能

3. 我之前一直用的 lineages os ,所以新手机必须与他差不多

4. 最好支付宝微信可以指纹

5. 对 root 以及 lsposed 有良好支持,例如,提供官方 root 工具,提供 ci/cd 测试环境,提供系统完整源码

一、从一次 HTTP 请求开始

在一个生产环境中,服务节点通常暴露了成百上千个 HTTP 接口对外提供服务。为了保证系统的稳定性,核心 HTTP 接口往往需要配置限流规则。给 HTTP 接口配置限流,可以防止突发或恶意的高并发请求耗尽服务器资源(如 CPU、内存、数据库连接等),从而避免服务崩溃或引发雪崩效应。

基础示例

假设我们有下面这样一个 HTTP 接口,需要给它配置限流规则:

@RestController
@RequiredArgsConstructor
@RequestMapping("/demo")
public class DemoController {

    @RequestMapping("/hello")
    @SentinelResource("test_sentinel")
    public String hello() {
        return "hello world";
    }
}

使用起来非常简单。首先我们可以选择给接口加上 @SentinelResource 注解(也可以不加,如果不加 Sentinel 客户端会使用请求路径作为资源名,详细原理在后面章节讲解),然后到流控控制台给该资源配置流控规则即可。

二、限流规则的加载

限流规则的生效,是从限流规则的加载开始的。聚焦到客户端的 RuleLoader 类,可以看到它支持了多种规则的加载:

  • 流控规则;
  • 集群限流规则;
  • 熔断规则;
  • ......

RuleLoader 核心逻辑

RuleLoader 类的核心作用是将这些规则加载到缓存中,方便后续使用:

public class RuleLoader {

    /**
     * 加载所有 Sentinel 规则到内存缓存
     *
     * @param sentinelRules 包含各种规则的配置对象
     */
    public static void loadRule(SentinelRules sentinelRules) {
        if (sentinelRules == null) {
            return;
        }

        // 加载流控规则
        FlowRuleManager.loadRules(sentinelRules.getFlowRules());
        // 加载集群流控规则
        RuleManager.loadClusterFlowRule(sentinelRules.getFlowRules());

        // 加载参数流控规则
        ParamFlowRuleManager.loadRules(sentinelRules.getParamFlowRules());
        // 加载参数集群流控规则
        RuleManager.loadClusterParamFlowRule(sentinelRules.getParamFlowRules());

        // 加载熔断规则
        DegradeRuleManager.loadRules(sentinelRules.getDegradeRules());

        // 加载参数熔断规则
        ParamDegradeRuleManager.loadRules(sentinelRules.getParamDegradeRules());

        // 加载系统限流规则
        SystemRuleManager.loadRules(sentinelRules.getSystemRules());
    }
}

流控规则加载详情

以流控规则的加载为例深入FlowRuleManager.loadRules 方法可以看到其完整的加载逻辑:

public static void loadRules(List<FlowRule> rules) {
    // 通过动态配置属性更新规则值
    currentProperty.updateValue(rules);
}

updateValue 方法负责通知所有监听器配置变更:

public boolean updateValue(T newValue) {
    // 如果新旧值相同,无需更新
    if (isEqual(value, newValue)) {
        return false;
    }
    RecordLog.info("[DynamicSentinelProperty] Config will be updated to: " + newValue);

    // 更新配置值
    value = newValue;
    // 通知所有监听器配置已更新
    for (PropertyListener<T> listener : listeners) {
        listener.configUpdate(newValue);
    }
    return true;
}

FlowPropertyListener 是流控规则变更的具体监听器实现:

private static final class FlowPropertyListener implements PropertyListener<List<FlowRule>> {

    @Override
    public void configUpdate(List<FlowRule> value) {
        // 构建流控规则映射表(按资源名分组)
        Map<String, List<FlowRule>> rules = FlowRuleUtil.buildFlowRuleMap(value);
        if (rules != null) {
            // 清空旧规则
            flowRules.clear();
            // 加载新规则
            flowRules.putAll(rules);
        }
        RecordLog.info("[FlowRuleManager] Flow rules received: " + flowRules);
    }
}

三、SentinelServletFilter 过滤器

在 Sentinel 中,所有的资源都对应一个资源名称和一个 Entry。Entry 可以通过对主流框架的适配自动创建,也可以通过注解的方式或调用 API 显式创建。Entry 是限流的入口类,通过 @SentinelResource 注解的限流本质上也是通过 AOP 的方式进行了对 Entry 类的调用。

Entry 的编程范式

Entry 类的标准使用方式如下:

// 资源名可使用任意有业务语义的字符串,比如方法名、接口名或其它可唯一标识的字符串
try (Entry entry = SphU.entry("resourceName")) {
    // 被保护的业务逻辑
    // do something here...
} catch (BlockException ex) {
    // 资源访问阻止,被限流或被降级
    // 在此处进行相应的处理操作
}

Servlet Filter 拦截逻辑

对于一个 HTTP 资源,在没有显式标注 @SentinelResource 注解的情况下,会有一个 Servlet Filter 类 SentinelServletFilter 统一进行拦截:

public class SentinelServletFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest sRequest = (HttpServletRequest) request;
        Entry urlEntry = null;

        try {
            // 获取并清理请求路径
            String target = FilterUtil.filterTarget(sRequest);

            // 统一 URL 清理逻辑
            // 对于 RESTful API,必须对 URL 进行清理(例如将 /foo/1 和 /foo/2 统一为 /foo/:id),
            // 否则上下文和资源的数量会超过阈值
            SentinelUrlCleaner urlCleaner = SentinelUrlCleaner.SENTINEL_URL_CLEANER;
            if (urlCleaner != null) {
                target = urlCleaner.clean(sRequest, target);
            }

            // 如果请求路径不为空且非安全扫描,则进入限流逻辑
            if (!StringUtil.isEmpty(target) && !isSecScan) {
                // 解析来源标识(用于来源限流)
                String origin = parseOrigin(sRequest);
                // 确定上下文名称
                String contextName = webContextUnify
                    ? WebServletConfig.WEB_SERVLET_CONTEXT_NAME
                    : target;

                // 使用 WEB_SERVLET_CONTEXT_NAME 作为当前 Context 的名字
                ContextUtil.enter(contextName, origin);

                // 根据配置决定是否包含 HTTP 方法
                if (httpMethodSpecify) {
                    String pathWithHttpMethod = sRequest.getMethod().toUpperCase() + COLON + target;
                    // 实际进入到限流统计判断逻辑,资源名是 "方法:路径"
                    urlEntry = SphU.entry(pathWithHttpMethod, ResourceTypeConstants.COMMON_WEB, EntryType.IN);
                } else {
                    // 实际进入到限流统计判断逻辑,资源名是请求路径
                    urlEntry = SphU.entry(target, ResourceTypeConstants.COMMON_WEB, EntryType.IN);
                }
            }

            // 继续执行后续过滤器
            chain.doFilter(request, response);

        } catch (BlockException e) {
            // 处理被限流的情况
            HttpServletResponse sResponse = (HttpServletResponse) response;
            // 返回限流页面或重定向到其他 URL
            WebCallbackManager.getUrlBlockHandler().blocked(sRequest, sResponse, e);

        } catch (IOException | ServletException | RuntimeException e2) {
            // 记录异常信息用于统计
            Tracer.traceEntry(e2, urlEntry);
            throw e2;

        } finally {
            // 释放 Entry 资源
            if (urlEntry != null) {
                urlEntry.exit();
            }
            // 退出当前上下文
            ContextUtil.exit();
        }
    }
}

四、SentinelResourceAspect 切面

如果在接口上标注了 @SentinelResource 注解,还会有另外的逻辑处理。Sentinel 定义了一个单独的 AOP 切面 SentinelResourceAspect 专门用于处理注解限流。

SentinelResource 注解定义

先来看看 @SentinelResource 注解的完整定义:

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
public @interface SentinelResource {

    /**
     * Sentinel 资源的名称(即资源标识)
     * 必填项,不能为空
     */
    String value() default "";

    /**
     * 资源的入口类型(入站 IN 或出站 OUT)
     * 默认为出站(OUT)
     */
    EntryType entryType() default EntryType.OUT;

    /**
     * 资源的分类(类型)
     * 自 1.7.0 版本起支持
     */
    int resourceType() default 0;

    /**
     * 限流或熔断时调用的 block 异常处理方法的名称
     * 默认为空(即不指定)
     */
    String blockHandler() default "";

    /**
     * blockHandler 所在的类
     * 如果与原方法不在同一个类,需要指定此参数
     */
    Class<?>[] blockHandlerClass() default {};

    /**
     * 降级(fallback)方法的名称
     * 默认为空(即不指定)
     */
    String fallback() default "";

    /**
     * 用作通用的默认降级方法
     * 该方法不能接收任何参数,且返回类型需与原方法兼容
     */
    String defaultFallback() default "";

    /**
     * fallback 所在的类
     * 如果与原方法不在同一个类,需要指定此参数
     */
    Class<?>[] fallbackClass() default {};

    /**
     * 需要被追踪并触发 fallback 的异常类型列表
     * 默认为 Throwable(即所有异常都会触发 fallback)
     */
    Class<? extends Throwable>[] exceptionsToTrace() default {Throwable.class};

    /**
     * 指定需要忽略的异常类型(即这些异常不会触发 fallback)
     * 注意:exceptionsToTrace 和 exceptionsToIgnore 不应同时使用;
     * 若同时存在,exceptionsToIgnore 优先级更高
     */
    Class<? extends Throwable>[] exceptionsToIgnore() default {};
}

实际使用示例

下面是一个完整的使用示例,展示了 @SentinelResource 注解的各种配置方式:

@RestController
public class SentinelController {

    @Autowired
    private ISentinelService service;

    @GetMapping(value = "/hello/{s}")
    public String apiHello(@PathVariable long s) {
        return service.hello(s);
    }
}

public interface ISentinelService {
    String hello(long s);
}

@Service
@Slf4j
public class SentinelServiceImpl implements ISentinelService {

    /**
     * Sentinel 提供了 @SentinelResource 注解用于定义资源
     *
     * @param s 输入参数
     * @return 返回结果
     */
    @Override
    // value:资源名称,必需项(不能为空)
    // blockHandler:对应处理 BlockException 的函数名称
    // fallback:用于在抛出异常的时候提供 fallback 处理逻辑
    @SentinelResource(value = "hello", blockHandler = "exceptionHandler", fallback = "helloFallback")
    public String hello(long s) {
        log.error("hello:{}", s);
        return String.format("Hello at %d", s);
    }

    /**
     * Fallback 函数
     * 函数签名与原函数一致,或加一个 Throwable 类型的参数
     */
    public String helloFallback(long s) {
        log.error("helloFallback:{}", s);
        return String.format("Halooooo %d", s);
    }

    /**
     * Block 异常处理函数
     * 参数最后多一个 BlockException,其余与原函数一致
     */
    public String exceptionHandler(long s, BlockException ex) {
        // Do some log here.
        log.error("exceptionHandler:{}", s);
        ex.printStackTrace();
        return "Oops, error occurred at " + s;
    }
}

SentinelResourceAspect 核心逻辑

@SentinelResource 注解由 SentinelResourceAspect 切面处理,核心逻辑如下:

@Aspect
public class SentinelResourceAspect extends AbstractSentinelAspectSupport {

    @Pointcut("@annotation(com.alibaba.csp.sentinel.annotation.SentinelResource)")
    public void sentinelResourceAnnotationPointcut() {
    }

    @Around("sentinelResourceAnnotationPointcut()")
    public Object invokeResourceWithSentinel(ProceedingJoinPoint pjp) throws Throwable {
        // 获取目标方法
        Method originMethod = resolveMethod(pjp);

        // 获取注解信息
        SentinelResource annotation = originMethod.getAnnotation(SentinelResource.class);
        if (annotation == null) {
            throw new IllegalStateException("Wrong state for SentinelResource annotation");
        }

        // 获取资源配置信息
        String resourceName = getResourceName(annotation.value(), originMethod);
        EntryType entryType = annotation.entryType();
        int resourceType = annotation.resourceType();

        Entry entry = null;
        try {
            // 创建限流入口
            entry = SphU.entry(resourceName, resourceType, entryType, pjp.getArgs());
            // 执行原方法
            Object result = pjp.proceed();
            return result;

        } catch (BlockException ex) {
            // 处理被限流异常
            return handleBlockException(pjp, annotation, ex);

        } catch (Throwable ex) {
            // 处理业务异常
            Class<? extends Throwable>[] exceptionsToIgnore = annotation.exceptionsToIgnore();
            // 优先检查忽略列表
            if (exceptionsToIgnore.length > 0 && exceptionBelongsTo(ex, exceptionsToIgnore)) {
                throw ex;
            }
            // 检查异常是否在追踪列表中
            if (exceptionBelongsTo(ex, annotation.exceptionsToTrace())) {
                traceException(ex);
                // 执行 fallback 逻辑
                return handleFallback(pjp, annotation, ex);
            }

            // 没有 fallback 函数可以处理该异常,直接抛出
            throw ex;

        } finally {
            // 释放 Entry 资源
            if (entry != null) {
                entry.exit(1, pjp.getArgs());
            }
        }
    }

    /**
     * 处理 BlockException
     *
     * blockHandler / blockHandlerClass 说明:
     * - blockHandler:对应处理 BlockException 的函数名称,可选项
     * - blockHandler 函数签名:与原方法相匹配并且最后加一个额外的参数,类型为 BlockException
     * - blockHandler 函数默认需要和原方法在同一个类中
     * - 若希望使用其他类的函数,则可以指定 blockHandlerClass 为对应的类的 Class 对象
     * - 注意:blockHandlerClass 中对应的函数必须为 static 函数,否则无法解析
     */
    protected Object handleBlockException(ProceedingJoinPoint pjp, SentinelResource annotation, BlockException ex)
            throws Throwable {

        // 执行 blockHandler 方法(如果配置了的话)
        Method blockHandlerMethod = extractBlockHandlerMethod(pjp, annotation.blockHandler(),
                annotation.blockHandlerClass());

        if (blockHandlerMethod != null) {
            Object[] originArgs = pjp.getArgs();
            // 构造参数:原方法参数 + BlockException
            Object[] args = Arrays.copyOf(originArgs, originArgs.length + 1);
            args[args.length - 1] = ex;

            try {
                // 根据 static 方法与否进行不同的调用
                if (isStatic(blockHandlerMethod)) {
                    return blockHandlerMethod.invoke(null, args);
                }
                return blockHandlerMethod.invoke(pjp.getTarget(), args);
            } catch (InvocationTargetException e) {
                // 抛出实际的异常
                throw e.getTargetException();
            }
        }

        // 如果没有 blockHandler,则尝试执行 fallback
        return handleFallback(pjp, annotation, ex);
    }

    /**
     * 处理 Fallback 逻辑
     *
     * fallback / fallbackClass 说明:
     * - fallback:fallback 函数名称,可选项,用于在抛出异常的时候提供 fallback 处理逻辑
     * - fallback 函数可以针对所有类型的异常(除了 exceptionsToIgnore 里面排除掉的异常类型)进行处理
     *
     * fallback 函数签名和位置要求:
     * - 返回值类型必须与原函数返回值类型一致
     * - 方法参数列表需要和原函数一致,或者可以额外多一个 Throwable 类型的参数用于接收对应的异常
     * - fallback 函数默认需要和原方法在同一个类中
     * - 若希望使用其他类的函数,则可以指定 fallbackClass 为对应的类的 Class 对象
     * - 注意:fallbackClass 中对应的函数必须为 static 函数,否则无法解析
     */
    protected Object handleFallback(ProceedingJoinPoint pjp, String fallback, String defaultFallback,
                                    Class<?>[] fallbackClass, Throwable ex) throws Throwable {
        Object[] originArgs = pjp.getArgs();

        // 执行 fallback 函数(如果配置了的话)
        Method fallbackMethod = extractFallbackMethod(pjp, fallback, fallbackClass);

        if (fallbackMethod != null) {
            // 构造参数:根据 fallback 方法的参数数量决定是否添加异常参数
            int paramCount = fallbackMethod.getParameterTypes().length;
            Object[] args;
            if (paramCount == originArgs.length) {
                args = originArgs;
            } else {
                args = Arrays.copyOf(originArgs, originArgs.length + 1);
                args[args.length - 1] = ex;
            }

            try {
                // 根据 static 方法与否进行不同的调用
                if (isStatic(fallbackMethod)) {
                    return fallbackMethod.invoke(null, args);
                }
                return fallbackMethod.invoke(pjp.getTarget(), args);
            } catch (InvocationTargetException e) {
                // 抛出实际的异常
                throw e.getTargetException();
            }
        }

        // 如果没有 fallback,尝试使用 defaultFallback
        return handleDefaultFallback(pjp, defaultFallback, fallbackClass, ex);
    }
}

五、流控处理核心逻辑

从入口函数开始,我们深入到流控处理的核心逻辑。

入口函数调用链

public class SphU {

    /**
     * 创建限流入口
     *
     * @param name 资源名称
     * @param resourceType 资源类型
     * @param trafficType 流量类型(IN 或 OUT)
     * @param args 参数数组
     * @return Entry 对象
     * @throws BlockException 如果被限流则抛出此异常
     */
    public static Entry entry(String name, int resourceType, EntryType trafficType, Object[] args)
            throws BlockException {
        return Env.sph.entryWithType(name, resourceType, trafficType, 1, args);
    }

    public static Entry entry(String name, EntryType trafficType, int batchCount) throws BlockException {
        return Env.sph.entry(name, trafficType, batchCount, OBJECTS0);
    }
}
public class CtSph implements Sph {

    @Override
    public Entry entry(String name, EntryType type, int count, Object... args) throws BlockException {
        StringResourceWrapper resource = new StringResourceWrapper(name, type);
        return entry(resource, count, args);
    }

    public Entry entry(ResourceWrapper resourceWrapper, int count, Object... args) throws BlockException {
        return entryWithPriority(resourceWrapper, count, false, args);
    }

    /**
     * 带优先级的入口方法,这是限流的核心逻辑
     */
    private Entry entryWithPriority(ResourceWrapper resourceWrapper, int count, boolean prioritized, Object... args)
            throws BlockException {
        Context context = ContextUtil.getContext();

        // 如果上下文数量超过阈值,则不进行规则检查
        if (context instanceof NullContext) {
            // NullContext 表示上下文数量超过了阈值,这里只初始化 Entry,不进行规则检查
            return new CtEntry(resourceWrapper, null, context);
        }

        // 如果没有上下文,使用默认上下文
        if (context == null) {
            context = InternalContextUtil.internalEnter(Constants.CONTEXT_DEFAULT_NAME);
        }

        // 如果全局开关关闭,则不进行规则检查
        if (!Constants.ON) {
            return new CtEntry(resourceWrapper, null, context);
        }

        // 获取或创建 ProcessorSlotChain(责任链)
        ProcessorSlot<Object> chain = lookProcessChain(resourceWrapper);

        /*
         * 如果资源(slot chain)数量超过 {@link Constants.MAX_SLOT_CHAIN_SIZE},
         * 则不进行规则检查
         */
        if (chain == null) {
            return new CtEntry(resourceWrapper, null, context);
        }

        // 创建 Entry 对象
        Entry e = new CtEntry(resourceWrapper, chain, context);

        try {
            // 执行责任链进行规则检查
            chain.entry(context, resourceWrapper, null, count, prioritized, args);
        } catch (BlockException e1) {
            // 如果被限流,释放 Entry 并抛出异常
            e.exit(count, args);
            throw e1;
        } catch (Throwable e1) {
            // 这不应该发生,除非 Sentinel 内部存在错误
            log.warn("Sentinel unexpected exception,{}", e1.getMessage());
        }
        return e;
    }
}

ProcessorSlotChain 功能插槽链

lookProcessChain 方法实际创建了 ProcessorSlotChain 功能插槽链。ProcessorSlotChain 采用责任链模式,将不同的功能(限流、降级、系统保护)组合在一起。

SlotChain 的获取与创建

ProcessorSlot<Object> lookProcessChain(ResourceWrapper resourceWrapper) {
    // 先从缓存中获取
    ProcessorSlotChain chain = chainMap.get(resourceWrapper);

    if (chain == null) {
        // 双重检查锁,保证线程安全
        synchronized (LOCK) {
            chain = chainMap.get(resourceWrapper);
            if (chain == null) {
                // Entry 大小限制
                if (chainMap.size() >= Constants.MAX_SLOT_CHAIN_SIZE) {
                    return null;
                }

                // 创建新的 SlotChain
                chain = SlotChainProvider.newSlotChain();

                // 使用不可变模式更新缓存
                Map<ResourceWrapper, ProcessorSlotChain> newMap =
                    new HashMap<ResourceWrapper, ProcessorSlotChain>(chainMap.size() + 1);
                newMap.putAll(chainMap);
                newMap.put(resourceWrapper, chain);
                chainMap = newMap;
            }
        }
    }
    return chain;
}

SlotChain 的构建

public class DefaultSlotChainBuilder implements SlotChainBuilder {

    @Override
    public ProcessorSlotChain build() {
        ProcessorSlotChain chain = new DefaultProcessorSlotChain();

        // 通过 SPI 加载所有 ProcessorSlot 并排序
        List<ProcessorSlot> sortedSlotList = SpiLoader.loadPrototypeInstanceListSorted(ProcessorSlot.class);

        for (ProcessorSlot slot : sortedSlotList) {
            // 只处理继承自 AbstractLinkedProcessorSlot 的 Slot
            if (!(slot instanceof AbstractLinkedProcessorSlot)) {
                RecordLog.warn("The ProcessorSlot(" + slot.getClass().getCanonicalName() +
                    ") is not an instance of AbstractLinkedProcessorSlot, can't be added into ProcessorSlotChain");
                continue;
            }

            // 将 Slot 添加到责任链尾部
            chain.addLast((AbstractLinkedProcessorSlot<?>) slot);
        }

        return chain;
    }
}

SlotChain 的功能划分

Slot Chain 可以分为两部分:

  • 统计数据构建部分(statistic):负责收集各种指标数据;
  • 判断部分(rule checking):根据规则判断是否限流。

官方架构图很好地解释了各个 Slot 的作用及其负责的部分。目前 ProcessorSlotChain 的设计是一个资源对应一个,构建好后缓存起来,方便下次直接取用。

各 Slot 的执行顺序

以下是 Sentinel 中各个 Slot 的默认执行顺序:

NodeSelectorSlot
    ↓
ClusterBuilderSlot
    ↓
StatisticSlot
    ↓
ParamFlowSlot
    ↓
SystemSlot
    ↓
AuthoritySlot
    ↓
FlowSlot
    ↓
DegradeSlot

NodeSelectorSlot - 上下文节点选择

这个功能插槽主要为资源下不同的上下文创建对应的 DefaultNode(实际用于统计指标信息)。解释一下Sentinel中的Node是什么,简单来说就是每个资源统计指标存放的容器,只不过内部由于不同的统计口径(秒级、分钟及)而分别有不同的统计窗口。Node在Sentinel不是单一的结构,而是总体上形成父子关系的树形结构。

不同的调用会有不同的 context 名称,如在当前 MVC 场景下,上下文为 sentinel_web_servlet_context。

public class NodeSelectorSlot extends AbstractLinkedProcessorSlot<Object> {

    /**
     * 同一个资源在不同上下文中的 DefaultNode 映射
     */
    private volatile Map<String, DefaultNode> map = new HashMap<String, DefaultNode>(10);

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, Object obj, int count,
                      boolean prioritized, Object... args) throws Throwable {
        // 从映射表中获取当前上下文对应的节点
        DefaultNode node = map.get(context.getName());

        if (node == null) {
            // 双重检查锁,保证线程安全
            synchronized (this) {
                node = map.get(context.getName());
                if (node == null) {
                    // 创建新的 DefaultNode
                    node = new DefaultNode(resourceWrapper, null);

                    // 使用写时复制更新缓存
                    HashMap<String, DefaultNode> cacheMap = new HashMap<String, DefaultNode>(map.size());
                    cacheMap.putAll(map);
                    cacheMap.put(context.getName(), node);
                    map = cacheMap;

                    // 构建调用树
                    ((DefaultNode) context.getLastNode()).addChild(node);
                }
            }
        }

        // 设置当前上下文的当前节点
        context.setCurNode(node);
        // 继续执行后续 Slot
        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }

    @Override
    public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
        fireExit(context, resourceWrapper, count, args);
    }
}

ClusterBuilderSlot - 集群节点构建

这个功能槽主要用于创建 ClusterNode。ClusterNode 和 DefaultNode 的区别是:

DefaultNode 是特定于上下文的(context-specific);

ClusterNode 是不区分上下文的(context-independent),用于统计该资源在所有上下文中的整体数据。

public class ClusterBuilderSlot extends AbstractLinkedProcessorSlot<DefaultNode> {

    /**
     * 全局 ClusterNode 映射表
     */
    private static volatile Map<ResourceWrapper, ClusterNode> clusterNodeMap = new HashMap<>();

    private static final Object lock = new Object();

    private volatile ClusterNode clusterNode = null;

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                      boolean prioritized, Object... args) throws Throwable {
        // 创建 ClusterNode(如果不存在)
        if (clusterNode == null) {
            synchronized (lock) {
                if (clusterNode == null) {
                    // 创建集群节点
                    clusterNode = new ClusterNode(resourceWrapper.getName(), resourceWrapper.getResourceType());

                    // 更新全局映射表
                    HashMap<ResourceWrapper, ClusterNode> newMap =
                        new HashMap<>(Math.max(clusterNodeMap.size(), 16));
                    newMap.putAll(clusterNodeMap);
                    newMap.put(node.getId(), clusterNode);

                    clusterNodeMap = newMap;
                }
            }
        }

        // 将 ClusterNode 设置到 DefaultNode 中
        node.setClusterNode(clusterNode);

        // 如果有来源标识,则创建 origin node
        if (!"".equals(context.getOrigin())) {
            Node originNode = node.getClusterNode().getOrCreateOriginNode(context.getOrigin());
            context.getCurEntry().setOriginNode(originNode);
        }

        // 继续执行后续 Slot
        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }
}

StatisticSlot - 统计插槽

StatisticSlot 是 Sentinel 最重要的类之一,用于根据规则判断结果进行相应的统计操作。

统计逻辑说明

entry 的时候:

依次执行后续的判断 Slot;

每个 Slot 触发流控会抛出异常(BlockException 的子类);

若有 BlockException 抛出,则记录 block 数据;

若无异常抛出则算作可通过(pass),记录 pass 数据。

exit 的时候:

若无 error(无论是业务异常还是流控异常),记录 complete(success)以及 RT,线程数 -1。

记录数据的维度:

线程数 +1;

记录当前 DefaultNode 数据;

记录对应的 originNode 数据(若存在 origin);

累计 IN 统计数据(若流量类型为 IN)。

public class StatisticSlot extends AbstractLinkedProcessorSlot<DefaultNode> {

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                      boolean prioritized, Object... args) throws Throwable {
        try {
            // 此位置会调用 SlotChain 中后续的所有 Slot,完成所有规则检测
            fireEntry(context, resourceWrapper, node, count, prioritized, args);

            // 请求通过,增加线程数和通过数
            // 代码运行到这个位置,就证明之前的所有 Slot 检测都通过了
            // 此时就可以统计请求的相应数据了

            // 增加线程数(+1)
            node.increaseThreadNum();
            // 增加通过请求的数量(这里涉及到滑动窗口算法)
            node.addPassRequest(count);

            // 省略其他统计逻辑...

        } catch (PriorityWaitException ex) {
            // 如果是优先级等待异常,记录优先级等待数
            node.increaseThreadNum();
            if (context.getCurEntry().getOriginNode() != null) {
                context.getCurEntry().getOriginNode().increaseThreadNum();
            }
            if (resourceWrapper.getEntryType() == EntryType.IN) {
                // 记录入站统计数据
                Constants.ENTRY_NODE.increaseThreadNum();
            }
            throw ex;

        } catch (BlockException e) {
            // 如果被限流,记录被限流数
            // 省略 block 统计逻辑...
            throw e;

        } catch (Throwable ex) {
            // 如果发生业务异常,记录异常数
            // 省略异常统计逻辑...
            throw ex;
        }
    }

    @Override
    public void exit(Context context, ResourceWrapper resourceWrapper, int count, Object... args) {
        // 若无 error(无论是业务异常还是流控异常),记录 complete(success)以及 RT,线程数-1
        // 记录数据的维度:线程数+1、记录当前 DefaultNode 数据、记录对应的 originNode 数据(若存在 origin)
        // 、累计 IN 统计数据(若流量类型为 IN)
        // 省略 exit 统计逻辑...
    }
}

StatisticNode 数据结构

到这里,StatisticSlot 的作用已经比较清晰了。接下来我们需要分析它的统计数据结构。fireEntry 调用向下的节点和之前的方式一样,剩下的节点主要包括:

  • ParamFlowSlot;
  • SystemSlot;
  • AuthoritySlot;
  • FlowSlot;
  • DegradeSlot;

其中比较常见的是流控和熔断:FlowSlot、DegradeSlot,所以下面我们着重分析 FlowSlot。

六、FlowSlot - 流控插槽

这个 Slot 主要根据预设的资源的统计信息,按照固定的次序依次生效。如果一个资源对应两条或者多条流控规则,则会根据如下次序依次检验,直到全部通过或者有一个规则生效为止。

FlowSlot 核心逻辑

@SpiOrder(-2000)
public class FlowSlot extends AbstractLinkedProcessorSlot<DefaultNode> {

    @Override
    public void entry(Context context, ResourceWrapper resourceWrapper, DefaultNode node, int count,
                      boolean prioritized, Object... args) throws Throwable {
        // 执行流控检查
        checkFlow(resourceWrapper, context, node, count, prioritized);

        // 继续执行后续 Slot
        fireEntry(context, resourceWrapper, node, count, prioritized, args);
    }

    // 省略其他方法...
}

checkFlow 方法详解

/**
 * 执行流控检查
 *
 * @param ruleProvider 规则提供者函数
 * @param resource 资源包装器
 * @param context 上下文
 * @param node 节点
 * @param count 请求数量
 * @param prioritized 是否优先
 * @throws BlockException 如果被限流则抛出异常
 */
public void checkFlow(Function<String, Collection<FlowRule>> ruleProvider, ResourceWrapper resource,
                      Context context, DefaultNode node, int count, boolean prioritized) throws BlockException {
    // 判断规则和资源不能为空
    if (ruleProvider == null || resource == null) {
        return;
    }

    // 获取指定资源的所有流控规则
    Collection<FlowRule> rules = ruleProvider.apply(resource.getName());

    // 逐个应用流控规则。若无法通过则抛出异常,后续规则不再应用
    if (rules != null) {
        for (FlowRule rule : rules) {
            if (!canPassCheck(rule, context, node, count, prioritized)) {
                // FlowException 继承 BlockException
                throw new FlowException(rule.getLimitApp(), rule);
            }
        }
    }
}

通过这里我们就可以得知,流控规则是通过 FlowRule 来完成的,数据来源是我们使用的流控控制台,也可以通过代码进行设置。

FlowRule 流控规则

每条流控规则主要由三个要素构成:

  • grade(阈值类型):按 QPS(每秒请求数)还是线程数进行限流;
  • strategy(调用关系策略):基于调用关系的流控策略;
  • controlBehavior(流控效果):当 QPS 超过阈值时的流量整形行为。
public class FlowRule extends AbstractRule {

    public FlowRule() {
        super();
        // 来源默认 Default
        setLimitApp(RuleConstant.LIMIT_APP_DEFAULT);
    }

    public FlowRule(String resourceName) {
        super();
        // 资源名称
        setResource(resourceName);
        setLimitApp(RuleConstant.LIMIT_APP_DEFAULT);
    }

    /**
     * 流控的阈值类型
     * 0: 线程数
     * 1: QPS
     */
    private int grade = RuleConstant.FLOW_GRADE_QPS;

    /**
     * 流控阈值
     */
    private double count;

    /**
     * 基于调用链的流控策略
     * STRATEGY_DIRECT: 直接流控(按来源)
     * STRATEGY_RELATE: 关联流控(关联资源)
     * STRATEGY_CHAIN: 链路流控(按入口资源)
     */
    private int strategy = RuleConstant.STRATEGY_DIRECT;

    /**
     * 关联流控模式下的关联资源
     */
    private String refResource;

    /**
     * 流控效果(流量整形行为)
     * 0: 默认(直接拒绝)
     * 1: 预热(Warm Up)
     * 2: 排队等待(Rate Limiter)
     * 3: 预热 + 排队等待(目前控制台没有)
     */
    private int controlBehavior = RuleConstant.CONTROL_BEHAVIOR_DEFAULT;

    /**
     * 预热时长(秒)
     */
    private int warmUpPeriodSec = 10;

    /**
     * 排队等待的最大超时时间(毫秒)
     */
    private int maxQueueingTimeMs = 500;

    /**
     * 是否为集群模式
     */
    private boolean clusterMode;

    /**
     * 集群模式配置
     */
    private ClusterFlowConfig clusterConfig;

    /**
     * 流量整形控制器
     */
    private TrafficShapingController controller;

    // 省略 getter/setter 方法...
}

七、滑动窗口算法

不管流控规则采用何种流控算法,在底层都需要有支持指标统计的数据结构作为支撑。在 Sentinel 中,用于支撑基于 QPS 等限流的数据结构是 StatisticNode。

StatisticNode 数据结构

public class StatisticNode implements Node {

    /**
     * 保存最近 1 秒内的统计数据
     * 每个桶(bucket)500ms,共 2 个桶
     */
    private transient volatile Metric rollingCounterInSecond =
        new ArrayMetric(SampleCountProperty.SAMPLE_COUNT, IntervalProperty.INTERVAL);

    /**
     * 保存最近 60 秒的统计数据
     * windowLengthInMs 被特意设置为 1000 毫秒,即每个桶代表 1 秒
     * 共 60 个桶,这样可以获得每秒精确的统计信息
     */
    private transient Metric rollingCounterInMinute =
        new ArrayMetric(60, 60 * 1000, false);

    // 省略其他字段和方法...
}

ArrayMetric 核心实现

ArrayMetric 是 Sentinel 中数据采集的核心,内部使用了 BucketLeapArray,即滑动窗口的思想进行数据的采集。

public class ArrayMetric implements Metric {

    /**
     * 滑动窗口数组
     */
    private final LeapArray<MetricBucket> data;

    public ArrayMetric(int sampleCount, int intervalInMs) {
        this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
    }

    public ArrayMetric(int sampleCount, int intervalInMs, boolean enableOccupy) {
        if (enableOccupy) {
            // 可抢占的滑动窗口,支持借用未来窗口的配额
            this.data = new OccupiableBucketLeapArray(sampleCount, intervalInMs);
        } else {
            // 普通滑动窗口
            this.data = new BucketLeapArray(sampleCount, intervalInMs);
        }
    }
}

这里有两种实现:

  • BucketLeapArray:普通滑动窗口,每个时间桶仅记录固定时间窗口内的指标数据;
  • OccupiableBucketLeapArray:扩展实现,支持"抢占"未来时间窗口的令牌或容量,在流量突发时允许借用后续窗口的配额,实现更平滑的限流效果。

BucketLeapArray - 滑动窗口实现

LeapArray 核心属性

LeapArray 是滑动窗口的基础类,其核心属性如下:

/**
 * 窗口大小(长度),单位:毫秒
 * 例如:1000ms
 */
private int windowLengthInMs;

/**
 * 样本数(桶的数量)
 * 例如:5(表示 5 个桶,每个 1000ms,总共 5 秒)
 */
private int sampleCount;

/**
 * 采集周期(总时间窗口长度),单位:毫秒
 * 例如:5 * 1000ms(5 秒)
 */
private int intervalInMs;

/**
 * 窗口数组,array 长度就是样本数 sampleCount
 */
protected final AtomicReferenceArray<WindowWrap<T>> array;

/**
 * 更新窗口数据的锁,保证数据的正确性
 */
private final ReentrantLock updateLock;

WindowWrap 窗口包装器

每个窗口包装器包含三个属性:

 public class WindowWrap<T> {

    /**
     * 窗口大小(长度),单位:毫秒
     * 与 LeapArray 中的 windowLengthInMs 一致
     */
    private final long windowLengthInMs;

    /**
     * 窗口开始时间戳
     * 它的值是 windowLengthInMs 的整数倍
     */
    private long windowStart;

    /**
     * 窗口数据(泛型 T)
     * Sentinel 目前只有 MetricBucket 类型,存储统计数据
     */
    private T value;
}

MetricBucket 指标桶

public class MetricBucket {

    /**
     * 计数器数组
     * 长度是需要统计的事件种类数,目前是 6 个
     * LongAdder 是线程安全的计数器,性能优于 AtomicLong
     */
    private final LongAdder[] counters;
    
    // 省略其他字段和方法...
}

滑动窗口工作原理

LeapArray 统计数据的基本思路:

创建一个长度为 n 的数组,数组元素就是窗口;

每个窗口包装了 1 个指标桶,桶中存放了该窗口时间范围内对应的请求统计数据;

可以想象成一个环形数组在时间轴上向右滚动;

请求到达时,会命中数组中的一个窗口,该请求的数据就会存到命中的这个窗口包含的指标桶中;

当数组转满一圈时,会回到数组的开头;

此时下标为 0 的元素需要重复使用,它里面的窗口数据过期了,需要重置,然后再使用。

获取当前窗口

LeapArray 获取当前时间窗口的方法:

 /**
 * 获取当前时间戳对应的窗口
 *
 * @return 当前时间的窗口
 */
public WindowWrap<T> currentWindow() {
    return currentWindow(TimeUtil.currentTimeMillis());
}

/**
 * 获取指定时间戳对应的窗口(核心方法)
 *
 * @param timeMillis 时间戳(毫秒)
 * @return 对应的窗口
 */
public WindowWrap<T> currentWindow(long timeMillis) {
    if (timeMillis < 0) {
        return null;
    }

    // 计算数组下标
    int idx = calculateTimeIdx(timeMillis);

    // 计算当前请求对应的窗口开始时间
    long windowStart = calculateWindowStart(timeMillis);

    // 无限循环,确保能够获取到窗口
    while (true) {
        // 取窗口
        WindowWrap<T> old = array.get(idx);

        if (old == null) {
            // 第一次使用,创建新窗口
            WindowWrap<T> window = new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));

            // CAS 操作,确保只初始化一次
            if (array.compareAndSet(idx, null, window)) {
                // 成功更新,返回创建的窗口
                return window;
            } else {
                // CAS 失败,让出时间片,等待其他线程完成初始化
                Thread.yield();
            }

        } else if (windowStart == old.windowStart()) {
            // 命中:取出的窗口的开始时间和本次请求计算出的窗口开始时间一致
            return old;

        } else if (windowStart > old.windowStart()) {
            // 窗口过期:本次请求计算出的窗口开始时间大于取出的窗口
            // 说明取出的窗口过期了,需要重置
            if (updateLock.tryLock()) {
                try {
                    // 成功获取锁,更新窗口开始时间,计数器重置
                    return resetWindowTo(old, windowStart);
                } finally {
                    updateLock.unlock();
                }
            } else {
                // 获取锁失败,让出时间片,等待其他线程更新
                Thread.yield();
            }

        } else if (windowStart < old.windowStart()) {
            // 异常情况:机器时钟回拨等
            // 正常情况不会进入该分支
            return new WindowWrap<T>(windowLengthInMs, windowStart, newEmptyBucket(timeMillis));
        }
    }
}

数据存储

在获取到窗口之后,就可以存储数据了。ArrayMetric 实现了 Metric 中存取数据的接口方法。

示例:存储 RT(响应时间)

/**
 * 添加响应时间数据
 *
 * @param rt 响应时间(毫秒)
 */
public void addRT(long rt) {
    // 获取当前时间窗口,data 为 BucketLeapArray
    WindowWrap<MetricBucket> wrap = data.currentWindow();

    // 计数
    wrap.value().addRT(rt);
}

/**
 * MetricBucket 的 addRT 方法
 *
 * @param rt 响应时间
 */
public void addRT(long rt) {
    // 记录 RT 时间对 rt 值
    add(MetricEvent.RT, rt);

    // 记录最小响应时间(非线程安全,但没关系)
    if (rt < minRt) {
        minRt = rt;
    }
}

/**
 * 通用的计数方法
 *
 * @param event 事件类型
 * @param n 增加的数量
 * @return 当前桶
 */
public MetricBucket add(MetricEvent event, long n) {
    counters[event.ordinal()].add(n);
    return this;
}

数据读取

示例:读取 RT(响应时间)

/**
 * 获取总响应时间
 *
 * @return 总响应时间
 */
public long rt() {
    // 触发当前窗口更新(处理过期窗口)
    data.currentWindow();

    long rt = 0;
    // 取出所有的 bucket
    List<MetricBucket> list = data.values();

    for (MetricBucket window : list) {
        rt += window.rt(); // 求和
    }
    return rt;
}

/**
 * 获取所有有效的窗口
 *
 * @return 有效窗口列表
 */
public List<T> values() {
    return values(TimeUtil.currentTimeMillis());
}

/**
 * 获取指定时间之前的所有有效窗口
 *
 * @param timeMillis 时间戳
 * @return 有效窗口列表
 */
public List<T> values(long timeMillis) {
    if (timeMillis < 0) {
        return new ArrayList<T>(); // 正常情况不会到这里
    }

    int size = array.length();
    List<T> result = new ArrayList<T>(size);

    for (int i = 0; i < size; i++) {
        WindowWrap<T> windowWrap = array.get(i);

        // 过滤掉没有初始化过的窗口和过期的窗口
        if (windowWrap == null || isWindowDeprecated(timeMillis, windowWrap)) {
            continue;
        }

        result.add(windowWrap.value());
    }
    return result;
}

/**
 * 判断窗口是否过期
 *
 * @param time 给定时间(通常是当前时间)
 * @param windowWrap 窗口包装器
 * @return 如果过期返回 true
 */
public boolean isWindowDeprecated(long time, WindowWrap<T> windowWrap) {
    // 给定时间与窗口开始时间超过了一个采集周期
    return time - windowWrap.windowStart() > intervalInMs;
}

OccupiableBucketLeapArray - 可抢占窗口

为什么需要 OccupiableBucketLeapArray?

假设一个资源的访问 QPS 稳定是 10,请求是均匀分布的:

在时间 0.0-1.0 秒区间中,通过了 10 个请求;

在 1.1 秒的时候,观察到的 QPS 可能只有 5,因为此时第一个时间窗口被重置了,只有第二个时间窗口有值;

当在秒级统计的情形下,用 BucketLeapArray 会有 0~50%的数据误这时就要用 OccupiableBucketLeapArray 来解决这个问题。

OccupiableBucketLeapArray 实现

从上面我们可以看到在秒级统计 rollingCounterInSecond 中,初始化实例时有两种构造参数:

public class OccupiableBucketLeapArray extends LeapArray<MetricBucket> {

    /**
     * 借用未来窗口的数组
     */
    private final FutureBucketLeapArray borrowArray;

    public OccupiableBucketLeapArray(int sampleCount, int intervalInMs) {
        super(sampleCount, intervalInMs);
        // 创建借用窗口数组
        this.borrowArray = new FutureBucketLeapArray(sampleCount, intervalInMs);
    }

    /**
     * 创建新的空桶
     * 会从 borrowArray 中借用数据
     */
    @Override
    public MetricBucket newEmptyBucket(long time) {
        MetricBucket newBucket = new MetricBucket();

        // 获取借用窗口的数据
        MetricBucket borrowBucket = borrowArray.getWindowValue(time);
        if (borrowBucket != null) {
            // 将借用数据复制到新桶中
            newBucket.reset(borrowBucket);
        }

        return newBucket;
    }

    /**
     * 重置窗口
     * 会从 borrowArray 中借用 pass 数据
     */
    @Override
    protected WindowWrap<MetricBucket> resetWindowTo(WindowWrap<MetricBucket> w, long time) {
        // 更新开始时间并重置值
        w.resetTo(time);

        MetricBucket borrowBucket = borrowArray.getWindowValue(time);
        if (borrowBucket != null) {
            // 重置桶值并添加借用的 pass 数据
            w.value().reset();
            w.value().addPass((int) borrowBucket.pass());
        } else {
            w.value().reset();
        }

        return w;
    }

    /**
     * 获取当前等待中的请求数量
     */
    @Override
    public long currentWaiting() {
        borrowArray.currentWindow();
        long currentWaiting = 0;
        List<MetricBucket> list = borrowArray.values();

        for (MetricBucket window : list) {
            currentWaiting += window.pass();
        }
        return currentWaiting;
    }

    /**
     * 添加等待中的请求数量
     *
     * @param time 时间
     * @param acquireCount 获取数量
     */
    @Override
    public void addWaiting(long time, int acquireCount) {
        WindowWrap<MetricBucket> window = borrowArray.currentWindow(time);
        window.value().add(MetricEvent.PASS, acquireCount);
    }
}

八、总结

至此,Sentinel 的基本情况都已经分析完成。以上内容主要讲解了 Sentinel 的核心处理流程,包括:

核心流程总结

  1. 规则加载:
  • 通过 RuleLoader 将各种规则(流控、熔断、系统限流等)加载到内存缓存中。
  1. 请求拦截:
  • 通过 SentinelServletFilter 过滤器拦截 HTTP 请求;
  • 通过SentinelResourceAspect切面处理 @SentinelResource 注解。
  1. 责任链处理:
  • 使用 ProcessorSlotChain 责任链模式组合多个功能插槽;
  • 每个插槽负责特定的功能(统计、流控、熔断等)。
  1. 流控判断:
  • FlowSlot 根据流控规则判断是否限流;
  • 通过滑动窗口算法统计 QPS、线程数等指标。
  1. 异常处理:
  • 被限流时抛出 BlockException;
  • 通过 blockHandler 或 fallback 处理异常。

核心技术点

  1. 责任链模式:
  • 通过 ProcessorSlotChain 将不同的限流功能组合在一起。
  1. 滑动窗口算法:
  • LeapArray 实现环形滑动窗口;
  • BucketLeapArray 普通滑动窗口;
  • OccupiableBucketLeapArray 可抢占窗口,支持借用未来配额。
  1. 数据结构:
  • DefaultNode:特定于上下文的统计节点;
  • ClusterNode:不区分上下文的集群统计节点;
  • StatisticNode:核心统计节点,包含秒级和分钟级统计。
  1. 限流算法:
  • QPS 限流:通过滑动窗口统计 QPS;
  • 线程数限流:通过原子计数器统计线程数;
  • 流控效果:快速失败、预热、排队等待等;

Sentinel 通过精心设计的架构,实现了高效、灵活、可扩展的流量控制能力,为微服务系统提供了强大的保护机制。

往期回顾

1.社区推荐重排技术:双阶段框架的实践与演进|得物技术

2.Flink ClickHouse Sink:生产级高可用写入方案|得物技术

3.服务拆分之旅:测试过程全揭秘|得物技术

4.大模型网关:大模型时代的智能交通枢纽|得物技术

5.从“人治”到“机治”:得物离线数仓发布流水线质量门禁实践

文 /万钧

关注得物技术,每周更新技术干货

要是觉得文章对你有帮助的话,欢迎评论转发点赞~

未经得物技术许可严禁转载,否则依法追究法律责任。

什么是泛域名证书?

泛域名证书,又称通配符SSL证书,是一种特殊的数字证书。它通过在域名前加上星号(*.)来表示,可以同时保护一个主域名及该主域名下所有的同一级子域名 。

核心特性与规则:

  • 一证多用:一张证书即可保护无数个同级子域名。例如购买 *.example.com 的证书,它可以同时用于 www.example.commail.example.comblog.example.com 等,未来新增子域名也无需重复申请。
  • 同级限制:需要注意的是,泛域名证书仅支持一级子域名。它无法保 护 test.forum.example.com 这样的二级或多级子域名 。
  • 简化管理:只需部署一次,即可让所有子域名继承“信任”,大大降低了证书续期和配置的运维工作量。

如何申请泛域名证书?(以JoySSL为例)

虽然阿里云、华为云等厂商均提供付费泛域名证书,但对于个人站长或中小企业来说,JoySSL 提供的永久免费泛域名证书是一个极具性价比的选择。它拥有全中文界面,且支持无限续签,非常适合国内用户 。

以下是具体的申请步骤:泛域名证书申请入口

1. 注册账号并选择产品

访问 JoySSL 官方网站,注册一个新账号。为了提高注册成功率,建议填写真实有效的邮箱及联系人信息。

  • 注册码:在注册过程中,活动页面要求填写注册码。可填写官方指定的 230970 或关注其官网最新公告获取,以确保能够申请永久免费证书。
2. 申请免费通配符证书

登录后,在证书列表中找到“免费体验版”或“永久免费SSL证书”分类,选择“免费版通配符SSL证书”。

  • 提交您需要保护的主域名(如 example.com),系统会自动识别为 *.example.com 的泛域名证书。
3. 域名所有权验证(DNS验证)

为了证明您拥有该域名的控制权,CA机构会要求验证。JoySSL 通常支持DNS验证(推荐)或文件验证 。

  • 操作:在订单详情中,会提供一条需要解析的TXT记录
  • 配置:您需要登录购买域名的服务商后台(如阿里云、腾讯云、GoDaddy等),在该域名的DNS解析设置中添加一条相应的TXT记录 。
  • 等待:添加完成后,回到JoySSL后台点击“验证”,通常几分钟内即可自动完成审核。
4. 下载与部署证书

验证通过后,证书会正式签发。

  • 下载证书包,根据您的服务器环境(如Nginx、Apache、IIS等)选择对应的版本。
  • 参照官方提供的安装文档,将证书文件上传至服务器并进行配置。重启Web服务后,您的网站主域名及所有子域名即可通过HTTPS安全访问。

总结

对于拥有多子域名架构的网站而言,泛域名证书不仅是成本控制的手段,更是提升管理效率的利器。通过 JoySSL 这类服务平台,你可以用极低的成本(甚至为零)快速获得由权威CA签发、浏览器信任的泛域名证书,让网站安全等级一步到位。

在移动应用分发领域,代码签名证书是确保应用合法性、完整性和用户信任的核心工具。无论是iOS的封闭生态还是Android的开放体系,未签名或签名不当的应用均可能被系统拦截,导致安装失败或触发安全警告。本文从技术原理、平台差异、常见误区及解决方案四个维度,系统梳理iOS/Android代码签名证书的避坑要点,并分析JoySSL在证书管理中的技术优势。

一、代码签名证书的核心价值与技术原理

1. 身份认证与代码完整性保护

代码签名证书通过非对称加密技术(如RSA 2048/4096或国密SM2算法)生成公私钥对,开发者使用私钥对应用代码进行签名,操作系统或应用商店通过公钥验证签名有效性。这一过程可实现两大核心功能:

  • 身份溯源:用户可通过证书信息确认应用开发者身份,避免“未知来源”警告;
  • 防篡改:任何代码修改均会导致签名失效,阻断中间人攻击或恶意代码注入。

2. 信任链构建机制

以iOS为例,其签名体系包含三层结构:

  • 开发者证书:由苹果CA颁发,绑定开发者账号;
  • 描述文件(Provisioning Profile) :关联证书、App ID和设备列表,控制应用分发范围;
  • 应用签名:使用证书私钥对编译后的IPA文件签名,生成唯一指纹。

Android的签名机制虽更开放,但同样依赖证书建立信任链。自Android 7.0起,系统强制要求APK签名使用V2/V3方案,通过全文件哈希校验提升安全性。

二、iOS代码签名:封闭生态的严格规范与常见陷阱

1. 证书类型与适用场景

iOS提供两类开发者证书:

  • 开发证书(Development Certificate) :用于调试阶段,绑定特定设备UDID,有效期1年;
  • 发布证书(Distribution Certificate) :用于App Store分发或企业内部分发,有效期3年。

避坑要点

  • 证书滥用:企业证书仅限内部使用,若用于分发非企业应用,可能被苹果吊销;
  • 设备数量限制:个人开发者账号最多绑定100台设备,超限需清理旧设备或升级账号类型;
  • Bundle ID冲突:App ID需与项目中的Bundle Identifier完全一致,否则导致签名失败。

2. 描述文件管理误区

描述文件是iOS签名的核心配置文件,其常见问题包括:

  • 过期未更新:描述文件有效期与证书绑定,需同步续期;
  • 权限遗漏:若应用需使用摄像头、相册等权限,需在描述文件中声明用途(如NSCameraUsageDescription);
  • 多环境混淆:开发、测试、发布环境需使用不同描述文件,避免交叉污染。

3. JoySSL在iOS签名中的优化实践

针对iOS证书管理痛点,JoySSL提供以下解决方案:

  • 自动化工具链:集成AppUploader等工具,支持在Windows/Linux环境生成证书和描述文件,减少对macOS的依赖;
  • 证书监控预警:通过自主监控平台提前30天预警证书到期,避免因过期导致应用下架;
  • 多账号管理:支持企业级账号体系,实现证书、描述文件和设备的集中化管控。

三、Android代码签名:开放生态的灵活性与安全挑战

1. 签名方案演进与兼容性

Android支持三种签名方案:

  • V1签名(JAR签名) :基于ZIP文件结构校验,易被破解;
  • V2签名(APK签名方案v2) :对APK进行全文件哈希校验,提升安全性;
  • V3签名(APK签名方案v3) :引入签名块旋转机制,支持签名信息动态更新。

避坑要点

  • 强制V2/V3签名:Android 7.0+设备默认要求V2签名,若未启用可能导致安装失败;
  • 多渠道包冲突:使用Gradle多渠道打包时,需确保所有渠道包使用同一证书签名,否则无法覆盖安装;
  • 密钥泄露风险:私钥丢失将导致应用无法更新,需立即吊销证书并重新签名。

2. 应用备案与签名信息绑定

根据中国工信部要求,Android应用需完成备案方可上架,备案信息包含:

  • 包名(Package Name) :全局唯一标识符(如com.example.app);
  • 公钥指纹:签名证书的SHA-256或MD5值;
  • 应用签名值:备案系统通过比对签名值防止应用被篡改。

避坑要点

  • 备案信息一致性:应用更新时需保持包名、公钥和签名值不变,否则需重新备案;
  • 签名证书轮换:若因安全原因更换证书,需同步更新备案信息,避免审核被拒。

3. JoySSL在Android签名中的技术优势

针对Android签名复杂性,JoySSL提供以下支持:

  • 多算法兼容:支持RSA、ECC和国密SM2算法,满足不同场景需求;
  • 时间戳服务:通过RFC 3161标准时间戳延长签名有效期,避免因证书过期导致旧版应用失效;
  • 自动化签名流水线:集成Jenkins、GitHub Actions等CI/CD工具,实现代码提交后自动签名和发布。

四、跨平台签名管理:统一化与自动化趋势

1. 代码签名证书的有效期新政

2026年3月起,全球代码签名证书有效期将从39个月缩短至460天(约15个月)。这一政策旨在降低证书被盗用风险,但对企业证书管理提出更高要求:

  • 续期频率增加:证书需每年更新,运维成本上升;
  • 自动化需求增长:手动续期易出错,需依赖自动化工具。

JoySSL通过自主监控平台实现证书生命周期管理,支持批量续期和自动部署,显著降低管理成本。

2. 区块链存证增强防篡改能力

部分CA机构(如JoySSL)开始探索将签名记录上链,通过分布式账本技术实现:

  • 签名信息不可篡改:每次签名操作均生成唯一哈希值并上链存储;
  • 审计追溯效率提升:监管机构可快速验证签名历史,打击恶意篡改行为。

五、总结:代码签名证书管理的核心原则

  1. 合规性优先:严格遵循苹果、谷歌及应用商店的签名规范,避免因违规导致应用下架;
  2. 密钥安全至上:采用HSM硬件或云密钥管理服务(如JoySSL的KMS)保护私钥,杜绝泄露风险;
  3. 自动化驱动效率:通过CI/CD工具链实现签名流程标准化,减少人为错误;
  4. 监控预警常态化:建立证书到期预警机制,确保业务连续性。

在移动应用竞争日益激烈的今天,代码签名证书不仅是合规门槛,更是建立用户信任、抵御安全威胁的核心基础设施。开发者需结合平台特性与技术趋势,选择权威CA机构(如JoySSL)的解决方案,以实现安全与效率的平衡。

RT ,想问下大佬们,比如个人或小工作室接小程序外包的话,ui 页面这部分是如何处理的呢,有没有 ai 工具可以生成这部分,如果可以生成 uniapp 代码更好了,html+tailwindcss 也可以,主要就是想要个能生成小程序 ui 页面的工具