[{"content":"Function Calling与MCP协议：AI助手工具调用的进化之路 一、Function Calling 2.1 要解决的问题 传统聊天大模型只会说话，没有工具调用能力，这使得大模型：\n无法感知环境：无法与外部数据源交互，如通过 API 查询网页、查看用户本地文件、访问远程数据库等等 无法改变环境：无法帮用户实际执行任务，如跑代码、发邮件、上传作业等 2.2 如何解决问题 后端 + LLM 传统方案\n工作流程\n存在的问题\n是否调用工具、调用什么工具由后端负责判断，逻辑复杂且容易误判。 AI 这么智能，为什么不让它来帮我判断？\n调用工具的参数由后端负责构建，难度很大。 AI 这么智能，为什么不让它来帮我生成参数？\nFunction Calling 方案 Function Calling 是什么\n广义的 Function Calling 是指让大模型能够调用外部工具的一种技术实现：先向大模型提供可用函数的列表及说明，由大模型在对话过程中智能判断是否需要调用函数，并自动生成调用所需的参数，最终用文字返回符合约定格式的函数调用请求。\n狭义的 Function Calling 特指大模型提供商在模型内部与 API 层面做了支持的一种能力，它最早由 OpenAI 引入：\n在模型层面：模型提供商需对大模型进行特别优化，使其具备根据上下文正确选择合适函数、生成有效参数的能力（比如有监督微调、强化学习）。 在 API 层面：模型提供商需额外开放对 Function Calling 的支持（比如 GPT API 中提供了一个 functions 参数）。 基于提示词的 Function Calling 工作流程\n# 你的角色 你是一个函数调用助手，我将提供多个函数的定义信息，包括函数名称、作用、参数及参数类型。 # 你的任务 - 根据用户的输入，判断是否需要调用某个函数 - 如果需要，请**严格按照以下格式**输出函数调用指令： ```json { \u0026#34;name\u0026#34;: \u0026#34;函数名\u0026#34;, \u0026#34;arguments\u0026#34;: { \u0026#34;参数名\u0026#34;: \u0026#34;参数值\u0026#34; } } 函数定义信息 get_weather 作用：查询指定城市的天气情况 参数：\n-city（string）：城市名称 get_time 作用：查询指定城市的当前时间 参数： city（string）：城市名称 \u0026ldquo;广州的天气怎么样？\u0026rdquo; { \u0026ldquo;name\u0026rdquo;: \u0026ldquo;get_weather\u0026rdquo;, \u0026ldquo;arguments\u0026rdquo;: { \u0026ldquo;city\u0026rdquo;: \u0026ldquo;广州\u0026rdquo; } }\n**存在的问题** 1. 输出格式不稳定。如调用指令中存在多余自然语言。 2. 容易出现幻觉。模型可能编造并不存在的函数名或参数。 大模型提供商能否对模型进行微调、强化学习，提升大模型在这一方面的能力？ 3. 对开发者依赖度高。函数描述、调用指令格式、提示词逻辑完全由开发者设计。 函数描述、调用指令格式能否由大模型提供商来指定？系统提示词中的\u0026#34;说明与规则\u0026#34;逻辑能否由大模型提供商来兜底？ 4. 上下文冗长，Token 消耗大。为确保调用逻辑正确，往往需要在 system prompt 中加入大量说明与规则。 #### 基于 API 的 Function Calling **工作流程** ![工作流程1.3](/images/工作流程1.3.jpg) 1. **用户发起提问** 用户通过自然语言提出问题，例如：\u0026#34;广州今天天气如何？适合出门吗？\u0026#34; 2. **后端第一次向大模型 API 发起请求，获取函数调用指令** 后端向大模型 API 传入用户原始输入、函数描述和其他上下文信息，获取调用指令。函数描述包括函数名称、用途说明、参数结构等。 ```json { \u0026#34;messages\u0026#34;: [ { \u0026#34;role\u0026#34;: \u0026#34;system\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;你是一个助手，可以根据用户的请求调用工具来获取信息。\u0026#34; }, { \u0026#34;role\u0026#34;: \u0026#34;user\u0026#34;, \u0026#34;content\u0026#34;: \u0026#34;广州今天天气如何？适合出门吗？\u0026#34; } ], \u0026#34;functions\u0026#34;: [ { \u0026#34;name\u0026#34;: \u0026#34;getWeather\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;获取指定城市的天气\u0026#34;, \u0026#34;parameters\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;object\u0026#34;, \u0026#34;properties\u0026#34;: { \u0026#34;location\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;城市名称，比如北京\u0026#34; }, \u0026#34;date\u0026#34;: { \u0026#34;type\u0026#34;: \u0026#34;string\u0026#34;, \u0026#34;description\u0026#34;: \u0026#34;日期，比如 2025-08-07\u0026#34; } }, \u0026#34;required\u0026#34;: [\u0026#34;location\u0026#34;, \u0026#34;date\u0026#34;] } } ] } 模型生成调用指令 模型会智能判断是否需要调用函数，选择合适的函数，并基于上下文自动生成结构化的调用指令（函数名 + 参数），例如：\n{ \u0026#34;function_call\u0026#34;: { \u0026#34;name\u0026#34;: \u0026#34;getWeather\u0026#34;, \u0026#34;arguments\u0026#34;: { \u0026#34;location\u0026#34;: \u0026#34;Guangzhou\u0026#34;, \u0026#34;date\u0026#34;: \u0026#34;2025-07-17\u0026#34; } } } 后端解析调用指令，并执行实际的函数调用 后端接收到模型返回的调用指令后，解析调用指令，得到函数名称和参数，执行对应的方法（如调用天气查询函数），并获取结果。\n后端第二次向大模型 API 发起请求，将刚才的调用结果和其他上下文信息一起传给模型，生成最终的回复 后端将函数执行结果 + 其他上下文信息（包括用户原始输入）传给模型，模型判断此时已有足够的信息回答问题，不再需要调用函数了，于是直接生成最终结果，例如：\u0026ldquo;广州今天35度，暴雨，建议在室内活动\u0026rdquo;。\n存在的问题\n后端应用适配不同大模型时存在大量冗余开发 可选模型有限 二、MCP 协议 3.1 要解决的问题 工具接入的冗余开发问题 AI 应用接入他人开发的新工具需完整 copy 代码和函数描述，接入几个就要 copy 几次。\n工具复用困难 环境问题导致 copy 的代码不一定能跑；很多企业不提供可供 copy 的源码；跨语言的代码 copy 了没用。\n3.2 如何解决问题 如果是你会怎么解决以上问题？（假设现在完全没有 MCP 协议）\n从问题出发\n→ 冗余开发、复用困难问题基本都是由 copy 代码带来的，怎么才能用上别人的方法，但又不用 copy 别人的代码？\n思路一：导包式接入，AI 应用开发者将工具代码拉到本地调用。\n可行吗？⚠️ 跨语言调用问题无法解决 跨语言问题是客观无解的吗？→ 能否本地另起一个进程运行该语言的执行环境，工具代码原封不动运行在对应语言环境中，AI 应用再通过进程间通信（如管道、套接字）获取标准化的返回结果？ 所以可行吗？✅ → 并且，这种接入方式更适合被称为\u0026quot;本地服务式接入\u0026quot; 思路二：远程服务式接入，工具开发者将工具独立部署，封装成标准化 API，约好统一的请求/响应格式，AI应用开发者只需按规定传入参数并解析返回值即可，不需要关心工具的实现语言、运行环境、内部逻辑。\n可行吗？✅ 从目标出发\n→ 接入工具、复用工具对开发者来说最理想的方式是什么？\n开发者只需添加一条配置（如工具的唯一标识或访问地址）就可以接入自己 / 别人的工具 → 当前的非理想状态是什么？\n当前每新增一个工具，开发者需要在链路中做两处人工适配： 补充工具描述 补充工具代码 → 从非理想状态到理想状态必须满足什么条件？\n要让\u0026quot;配置替代人工适配\u0026quot;，必须满足以下两点： 新增配置后，AI 应用后端要能根据配置自动获取工具的描述信息 新增配置后，AI 应用后端要能根据配置自动定位工具调用入口并执行调用 从问题到技术需求\n→ 本地服务式接入场景\n如何在任意 AI 应用中通过一条标准化配置： 拉取任意工具的包到本地，并在本地起一个进程，将工具作为一个服务运行起来（只要工具开发者有提供对应的包） 自动获取工具的描述信息、自动完成调用过程（通过本地进程间通信） → 远程服务式接入场景\n如何在任意 AI 应用中通过一条标准化配置： 访问任意工具的远程服务（只要工具开发者有对外提供服务） 自动获取工具的描述信息、自动完成调用过程（通过远程服务调用） 技术方案思路\n工具与AI 应用必须解耦合。 工具与AI 应用之间的交互必须标准化。 AI 应用和工具服务的通信协议需要统一（本地进程间的通信协议 / 远程服务调用的协议） AI 应用和工具服务的接口定义需要统一（需要提供哪些接口、接口需包含哪些参数） AI 应用和工具服务的数据交换格式需要统一（接口的请求 / 响应格式等) AI 应用接入工具的配置内容需要进行标准化定义 所有工具服务必须提供标准化的接入方式，以支持通过标准化配置即可加载工具 所有 AI 应用内部需实现标准化的工具加载调用逻辑，以支持通过标准化配置即可加载工具 系统架构设计\n3.3 MCP 协议是什么 诞生：\n2024 年 11 月由 Anthropic（一家美国人工智能初创公司）提出，官方文档。\n定义：\nMCP is an open protocol that standardizes how applications provide context to large language models (LLMs). Think of MCP like a USB-C port for AI applications. Just as USB-C provides a standardized way to connect your devices to various peripherals and accessories, MCP provides a standardized way to connect AI models to different data sources and tools. MCP enables you build agents and complex workflows on top of LLMs and connects your models with the world. [1]\nMCP 是一个开放协议，用于标准化应用程序向大语言模型（LLM）提供上下文的方式。你可以把 MCP 想象成 AI 应用的 USB-C 接口——正如 USB-C 提供了一种将设备连接到各种外设和配件的标准化方式一样，MCP 提供了一种将 AI 模型连接到不同数据源和工具的标准化方式。借助 MCP，你可以在 LLM 之上构建智能体和复杂工作流，并将你的模型与外部世界相连接。\n如何理解： 应用程序：集成了 LLM 的具体应用。包括各家大模型的在线对话网站、集成了大模型的IDE（如 Claude desktop）、各种 Agent（比如 Cursor 就是一个 Agent）、以及其他接入了大模型的普通应用。 上下文：指的是模型在决策时可访问的所有信息，如当前用户输入、历史对话信息、外部工具（tool）信息、外部数据源（resource）信息、提示词（prompt）信息等等（这里重点只讲工具）。 和传统 API 的区别？\n3.4 MCP 核心架构 MCP follows a client-server architecture where an MCP host — an AI application like Claude Code or Claude Desktop — establishes connections to one or more MCP servers. The MCP host accomplishes this by creating one MCP client for each MCP server. Each MCP client maintains a dedicated one-to-one connection with its corresponding MCP server.The key participants in the MCP architecture are:\nMCP Host: The AI application that coordinates and manages one or multiple MCP clients MCP Client: A component that maintains a connection to an MCP server and obtains context from an MCP server for the MCP host to use MCP Server: A program that provides context to MCP clients For example: Visual Studio Code acts as an MCP host. When Visual Studio Code establishes a connection to an MCP server, such as the Sentry MCP server, the Visual Studio Code runtime instantiates an MCP client object that maintains the connection to the Sentry MCP server. When Visual Studio Code subsequently connects to another MCP server, such as the local filesystem server, the Visual Studio Code runtime instantiates an additional MCP client object to maintain this connection, hence maintaining a one-to-one relationship of MCP clients to MCP servers.Note that MCP server refers to the program that serves context data, regardless of where it runs. MCP servers can execute locally or remotely. For example, when Claude Desktop launches the filesystem server, the server runs locally on the same machine because it uses the STDIO transport. This is commonly referred to as a \u0026ldquo;local\u0026rdquo; MCP server. The officialSentry MCP server runs on the Sentry platform, and uses the Streamable HTTP transport. This is commonly referred to as a \u0026ldquo;remote\u0026rdquo; MCP server. [2]\nMCP遵循客户端-服务器架构，其中 MCP Host——Claude Code 或 Claude Desktop 等AI应用程序——与一个或多个MCP Server 建立连接。MCP 主机通过为每个 MCP Server 创建一个 MCP Client 来实现这一目标。每个 MCP Client 都与相应的 MCP Server 保持专用的一对一连接。MCP架构的主要组成者是：\nMCP Host：协调和管理一个或多个 MCP Server 的人工智能应用程序 MCP Client：一个组件，用于维护与 MCP 服务器的连接，并从 MCP 服务器获取上下文，供 MCP 主机使用 MCP Server：一个为 MCP Client 提供上下文的程序 例如：Visual Studio Code 充当 MCP 主机。当 Visual Studio Code 建立与MCP服务器（如Sentry MCP服务器）的连接时，Visual Studio Code 运行时实例化了维护与Sentry MCP服务器连接的MCP客户端对象。当Visual Studio Code 随后连接到另一个MCP服务器时，例如本地文件系统服务器，Visual Studio Code 运行时实例化一个额外的MCP客户端对象来维护此连接，从而保持MCP客户端与MCP服务器的一对一关系。\n3.5 MCP 的传输协议 MCP supports two transport mechanisms:\nStdio transport: Uses standard input/output streams for direct process communication between local processes on the same machine, providing optimal performance with no network overhead. Streamable HTTP transport: Uses HTTP POST for client-to-server messages with optional Server-Sent Events for streaming capabilities. This transport enables remote server communication and supports standard HTTP authentication methods including bearer tokens, API keys, and custom headers. MCP recommends using OAuth to obtain authentication tokens. The transport layer abstracts communication details from the protocol layer, enabling the same JSON-RPC 2.0 message format across all transport mechanisms. [3]\nStdio 传输 Stdio 传输本质上是本地进程间通信（IPC）的一种形式，它最常用的底层机制就是管道（pipe）。\n什么是 stdio？ stdio（standard I/O）是进程的标准输入/输出接口。每个进程启动时，操作系统会给它分配 三个文件描述符： 0 → stdin （标准输入，默认是键盘） 1 → stdout （标准输出，默认是屏幕） 2 → stderr （标准错误输出，默认是屏幕） 程序里的 printf、scanf、cin、cout、read、write 都是通过这些接口和外界交换数据的。 什么是管道（pipe）？ 管道是操作系统内核提供的一种进程间通信（IPC）机制，它允许一个进程的输出直接作为另一个进程的输入，实现数据在两个进程之间的流动。 总结：什么是 Stdio 传输 ？ 所谓 Stdio 传输，就是通过标准输入和标准输出这两个数据流来传输数据、通过管道来连接两个进程的标准输入/输出接口，使得一个进程的输出直接传给另一个进程输入，实现进程间数据传输（本质上是一个基于字节流的全双工通信通道）\nstdio 是接口，管道是连接这接口的通道。\n举例：\nps aux [键盘] → shell → ps(stdin) → ps(stdout) → [屏幕] ps aux | grep python # ps 和 grep 在执行时都会各自成为一个独立的进程 # ps aux 列出进程 → grep python 过滤后留下包含 \u0026#34;python\u0026#34; 的进程 # 管道 | 把 ps aux 的 stdout 作为 grep python 的 stdin [键盘] → shell → ps(stdin) → ps(stdout) → grep(stdin) → grep(stdout) → 屏幕 为什么在这么多本地进程间通信的方式中选了 Stdio 传输？\nHTTP + SSE 传输（旧方案，2024.10） 客户端通过 HTTP POST 向服务端发请求，服务端通过 SSE 通道返回响应结果。\nSSE（Server-Sent Events服务器发送事件），是一种服务器单向推送数据给客户端的技术，基于 HTTP 协议。 基本原理 客户端先向服务端发起一个普通的 HTTP 请求。 服务端保持这个连接不断开，以 text/event-stream 作为响应类型，源源不断地往里写数据。 客户端收到数据后会触发相应的事件回调（比如浏览器前端实时更新界面）。 和普通 HTTP 的核心差异 支持服务端主动、流式地推送消息 为什么在这么多远程服务调用的协议中选了 HTTP + SSE？\n服务端推送的必要性：MCP Server 中的工具发生了更新，需要主动向 MCP Client 推送通知 Why Notifications Matter\nThis notification system is crucial for several reasons:\nDynamic Environments: Tools may come and go based on server state, external dependencies, or user permissions Efficiency: Clients don\u0026rsquo;t need to poll for changes; they\u0026rsquo;re notified when updates occur Consistency: Ensures clients always have accurate information about available server capabilities Real-time Collaboration: Enables responsive AI applications that can adapt to changing contexts This notification pattern extends beyond tools to other MCP primitives, enabling comprehensive real-time synchronization between clients and servers. [4]\nStreamable HTTP 传输（新方案，2025.03） HTTP + SSE 传输方案的升级版，目前正在逐步取代原有的 HTTP + SSE 传输方案\nStreamable HTTP 并不是一个标准协议名，而是一个通用描述，指的是基于 HTTP 协议的\u0026quot;可流式传输\u0026quot;技术。它的核心思想是：在一个 HTTP 连接里，服务端可以持续不断地发送数据给客户端，客户端边接收边处理，类似\u0026quot;流\u0026quot;一样。与传统 HTTP 请求响应\u0026quot;一次性完成\u0026quot;不同，Streamable HTTP 保持连接不关闭，数据分片持续传输。常见实现方式包括： HTTP/1.1 长连接 + 分块传输编码（Chunked Transfer Encoding） HTTP/2 流式数据 HTTP/3 QUIC 流式传输 为什么 HTTP + SSE 要升级成 Streamable HTTP ？\n数据格式限制问题：SSE 的 Content-Type: text/event-stream 只支持文本格式；Streamable HTTP 的Content-Type支持任意格式，如 JSON、HTML、二进制等，更适合 AI 场景（可能要传 JSON + 音频 + 图片） 跨平台兼容问题：SSE 支持的客户端主要是浏览器端和少量语言库；而 Streamable HTTP 支持多种客户端。 性能问题：SSE 是基于 HTTP/1.1 长连接，Streamable HTTP 可以基于 HTTP/2/3 ，支持多路复用和双向流。且 HTTP/2/3 的流控制和优先级机制使得高吞吐和低延迟成为可能；SSE 消息只能文本格式，Streamable HTTP 支持其他采用更紧凑的编码方式（比如二进制分包、压缩等）。 必须选用以上传输协议吗？\n——No，因为无论哪种传输方式，都只是把各种工具的不同接入方式统一起来，对外暴露一种协议的接口而已。\n3.6 回顾：技术方案最终是怎么实现的 工具与AI 应用必须解耦合。 ✅ 客户端 - 服务端架构（MCP Host、MCP Client、MCP Server）\n工具与AI 应用之间的交互必须标准化。\nAI 应用和工具服务的通信协议需要统一（本地进程间的通信协议 / 远程服务调用的协议） ✅ 本地：Stdio 传输；远程：HTTP + SSE 或 Streamable HTTP\nAI 应用和工具服务的接口定义需要统一（需要提供哪些接口、接口需包含哪些参数） ✅ 工具服务需提供的接口：1. tools/list（用于返回方法列表） 2. tools/call（用于执行方法并返回结果。3. notifications/tools/list_changed（服务端主动推送，用于告知客户端方法更新）[5] ✅接口参数定义，见文档。以 tools/list 为例：\nAI 应用和工具服务的数据交换格式需要统一（接口的请求 / 响应格式等) ✅ JSON-RPC 2.0 [6] [7] JSON-RPC 2.0 是一种轻量级的远程过程调用（RPC）协议，基于 JSON 格式进行通信，主要特点是所有消息都是 JSON 格式，便于解析和跨语言使用。\nAI 应用接入工具的配置内容需要进行标准化定义 以接入高德地图 MCP Server 为例，参考文档：https://lbs.amap.com/api/mcp-server/gettingstarted（来自ModelScope 的 MCP Server 市场：https://modelscope.cn/mcp） ✅ 本地服务式接入（基于 Stdio 协议）\n// 在 mcpServers 配置中新增一个叫 \u0026#34;amap-maps\u0026#34;（你自己起名）的 MCP Server // 通过 npx -y @amap/amap-maps-mcp-server 命令，运行高德官方提供的 MCP server // 运行时的环境变量是 \u0026#34;AMAP_MAPS_API_KEY\u0026#34;: \u0026#34;你申请的 API Key\u0026#34; { \u0026#34;mcpServers\u0026#34;: { \u0026#34;amap-maps\u0026#34;: { \u0026#34;command\u0026#34;: \u0026#34;npx\u0026#34;, \u0026#34;args\u0026#34;: [\u0026#34;-y\u0026#34;, \u0026#34;@amap/amap-maps-mcp-server\u0026#34;], \u0026#34;env\u0026#34;: { \u0026#34;AMAP_MAPS_API_KEY\u0026#34;: \u0026#34;您在高德官网上申请的key\u0026#34; } } } } ✅ 远程服务式接入（基于 SSE 协议） { \u0026#34;mcpServers\u0026#34;: { \u0026#34;amap-maps-streamableHTTP\u0026#34;: { \u0026#34;url\u0026#34;: \u0026#34;https://mcp.amap.com/mcp?key=您在高德官网上申请的key\u0026#34; } } } 所有工具服务必须提供标准化的接入方式，以支持通过标准化配置即可加载工具 ✅ 遵循 MCP 协议开发 MCP Server，对外提供标准的接入方式。多语言 SDK 地址。 所有 AI 应用内部需实现标准化的工具加载调用逻辑，以支持通过标准化配置即可加载工具 ✅ 使用官方 SDK，在 AI 应用后端项目中实例化 MCP Client，调用 SDK 方法和 Server 交互。 3.7 MCP 工作流程 值得一提的是，MCP 协议和 Function Calling 之间绝不是\u0026quot;技术递进\u0026quot;的关系。所谓\u0026quot;MCP 协议会取代 Function Calling\u0026quot;的说法，其实是一种不严谨的表达。\n3.8 What\u0026rsquo;s More? 看 MCP 协议官方文档：https://modelcontextprotocol.io/docs/getting-started/intro，思考为何这么设计； 通过官方 SDK，尝试编写一个 MCP Server 并启动服务；初始化一个后端应用，创建 MCP Client，完成 Client - Server 完整交互流程的开发。MCP SDK 地址：https://modelcontextprotocol.io/docs/sdk 找一个开源的支持了 MCP 协议的 Agent 框架，追溯其中涉及到 MCP Client 、MCP Server 逻辑的所有代码。 ","permalink":"https://asterzephyr.github.io/posts/fc-mcp/","summary":"\u003ch1 id=\"function-calling与mcp协议ai助手工具调用的进化之路\"\u003eFunction Calling与MCP协议：AI助手工具调用的进化之路\u003c/h1\u003e\n\u003ch2 id=\"一function-calling\"\u003e一、Function Calling\u003c/h2\u003e\n\u003ch3 id=\"21-要解决的问题\"\u003e2.1 要解决的问题\u003c/h3\u003e\n\u003cp\u003e传统聊天大模型只会说话，没有工具调用能力，这使得大模型：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e无法感知环境\u003c/strong\u003e：无法与外部数据源交互，如通过 API 查询网页、查看用户本地文件、访问远程数据库等等\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e无法改变环境\u003c/strong\u003e：无法帮用户实际执行任务，如跑代码、发邮件、上传作业等\u003c/li\u003e\n\u003c/ol\u003e\n\u003ch3 id=\"22-如何解决问题\"\u003e2.2 如何解决问题\u003c/h3\u003e\n\u003ch4 id=\"后端--llm\"\u003e后端 + LLM\u003c/h4\u003e\n\u003cp\u003e\u003cstrong\u003e传统方案\u003c/strong\u003e\u003c/p\u003e","title":"Function Calling与MCP协议：AI助手工具调用的进化之路"},{"content":"Word2Vec深度解析：让计算机理解词汇语义的黑魔法 前段时间在研究一些老派的NLP模型，重新翻了翻Word2Vec的论文。虽然现在大家都在用BERT、GPT这些Transformer模型，但Word2Vec这个十年前的模型至今仍然在很多场景下发光发热。\nWord2Vec解决的问题其实很朴素：怎么让计算机理解词汇的含义？在深度学习之前，我们只能用one-hot这种稀疏表示，每个词就是一个巨大向量中的一个1。但Word2Vec说，不如我们让相似的词有相似的向量表示吧。\n今天就来深入挖掘一下Word2Vec是怎么做到这个\u0026quot;黑魔法\u0026quot;的。\n核心理念：词汇的\u0026quot;朋友圈\u0026quot;决定了它的性格 Word2Vec的想法很简单，但很天才：一个词经常跟什么词一起出现，就能判断这个词的意思。\n这就像现实生活中，你经常跟什么人在一起，别人就大概能判断你是什么样的人。\n经常一起出现的词 → 语义相似 → 向量表示相近 比如\u0026quot;猫\u0026quot;和\u0026quot;狗\u0026quot;这两个词：\n它们都经常跟\u0026quot;宠物\u0026quot;、\u0026ldquo;可爱\u0026rdquo;、\u0026ldquo;毛茸茸\u0026quot;一起出现 所以Word2Vec会学到它们的向量很相近 而\u0026quot;飞机\u0026quot;的向量就会离它们很远 从词汇到数字：高维空间的魔法 Word2Vec把每个词变成一个高维向量（比如100维）：\n# 每个词都有自己的\u0026#34;数字身份证\u0026#34; word_vectors = { \u0026#34;猫\u0026#34;: [0.1, -0.3, 0.8, 0.2, ...], # 100维向量 \u0026#34;狗\u0026#34;: [0.2, -0.1, 0.7, 0.3, ...], # 和猫很相似 \u0026#34;飞机\u0026#34;: [-0.5, 0.8, -0.2, 0.1, ...], # 和猫狗距离很远 } 神奇的是，这些看似随机的数字组合，竟然能精确地表示词汇的语义关系。\n两种训练方法：CBOW vs Skip-gram Word2Vec有两种训练方式，就像学语言有两种思路：\nCBOW：根据上下文猜词汇 CBOW的思路是：给你几个词，让你猜中间缺的那个词。\n\u0026#34;我爱吃___苹果\u0026#34; → 模型要猜出\u0026#34;红色的\u0026#34; 实现上就是用周围的词来预测中心词：\nclass CBOWModel: def __init__(self, vocab_size, embedding_dim, context_size): self.vocab_size = vocab_size # 有多少个不同的词 self.embedding_dim = embedding_dim # 每个词用多少维向量表示 self.context_size = context_size # 上下文窗口大小 # 这就是我们要学习的词向量矩阵 self.W1 = np.random.randn(vocab_size, embedding_dim) * 0.01 # 用来做最终预测的权重矩阵 self.W2 = np.random.randn(embedding_dim, vocab_size) * 0.01 self.b1 = np.zeros(embedding_dim) self.b2 = np.zeros(vocab_size) def forward(self, context_indices): \u0026#34;\u0026#34;\u0026#34;前向传播：从上下文预测中心词\u0026#34;\u0026#34;\u0026#34; # 把上下文词汇都转成one-hot向量 context_vectors = np.zeros((len(context_indices), self.vocab_size)) for i, idx in enumerate(context_indices): context_vectors[i, idx] = 1 # 取上下文词向量的平均值 hidden = np.mean(context_vectors @ self.W1, axis=0) + self.b1 # 计算对每个词的预测概率 output = hidden @ self.W2 + self.b2 # softmax归一化，得到概率分布 exp_output = np.exp(output - np.max(output)) probabilities = exp_output / np.sum(exp_output) return probabilities, hidden CBOW比较擅长处理高频词，因为它看的是整体的上下文模式。\nSkip-gram：给一个词，猜它的朋友圈 Skip-gram的思路正好相反：给你一个词，让你猜它周围会出现什么词。\n\u0026#34;苹果\u0026#34; → 可能的上下文：[\u0026#34;红色的\u0026#34;, \u0026#34;甜\u0026#34;, \u0026#34;水果\u0026#34;, \u0026#34;吃\u0026#34;] 这种方式对低频词更友好，因为即使一个词出现次数不多，我们也能从它的每次出现中学到信息：\nclass SkipGramModel: def __init__(self, vocab_size, embedding_dim): self.vocab_size = vocab_size self.embedding_dim = embedding_dim # 输入词向量矩阵（这个就是我们最终要的词向量） self.W1 = np.random.randn(vocab_size, embedding_dim) * 0.01 # 输出矩阵（用来预测上下文词） self.W2 = np.random.randn(embedding_dim, vocab_size) * 0.01 self.b1 = np.zeros(embedding_dim) self.b2 = np.zeros(vocab_size) def forward(self, input_idx): \u0026#34;\u0026#34;\u0026#34;前向传播：从中心词预测上下文\u0026#34;\u0026#34;\u0026#34; # 输入是一个词的one-hot向量 input_vector = np.zeros(self.vocab_size) input_vector[input_idx] = 1 # 得到这个词的向量表示 hidden = input_vector @ self.W1 + self.b1 # 预测上下文中每个位置的词 output = hidden @ self.W2 + self.b2 # 计算概率分布 exp_output = np.exp(output - np.max(output)) probabilities = exp_output / np.sum(exp_output) return probabilities, hidden 在实践中，Skip-gram用得更多，因为它能更好地处理稀有词汇和专业术语。\n负采样：让训练快1000倍的神器 原始的Word2Vec有个大问题：计算量太大了。\n想象一下，如果你的词典有10万个词，每次训练都要计算这10万个词的概率，那训练会慢成什么样子？\n问题在哪里？ 传统方法需要对每个词都算一遍概率：\n# 传统softmax：每次都要计算10万个词的概率 def traditional_softmax(output, target_idx): exp_output = np.exp(output) # 10万次计算 probabilities = exp_output / np.sum(exp_output) # 又是10万次 return -np.log(probabilities[target_idx]) 这太慢了！\n负采样的聪明想法 负采样说：我们不需要每次都看所有词，只需要：\n看看正确答案（正样本） 随机挑几个错误答案（负样本） 让模型学会区分对错就行了 # 负采样：只看正确答案 + 5个错误答案 def negative_sampling_loss(output, target_idx, negative_indices): # 正样本：这个词应该出现的概率要高 pos_score = output[target_idx] pos_loss = -np.log(1 / (1 + np.exp(-pos_score))) # 负样本：这些词不应该出现，概率要低 neg_loss = 0 for neg_idx in negative_indices: # 只有5个，不是10万个！ neg_score = output[neg_idx] neg_loss += -np.log(1 / (1 + np.exp(neg_score))) return pos_loss + neg_loss 这样计算量从10万降到了6（1个正样本+5个负样本），训练速度提升了几千倍！\n怎么选负样本？ 负样本不能乱选，有个小技巧：高频词容易被选为负样本，低频词不容易被选中。\n这样做的原因是，高频词（比如\u0026quot;的\u0026rdquo;、\u0026ldquo;是\u0026rdquo;、\u0026ldquo;在\u0026rdquo;）在大部分上下文中都不应该出现，拿它们做负样本能让模型学得更好。\n# 简化的负采样逻辑 def sample_negative_words(target_idx, word_freqs, num_negatives=5): \u0026#34;\u0026#34;\u0026#34;采样负样本，高频词更容易被选中\u0026#34;\u0026#34;\u0026#34; # 词频的0.75次方，这是经验值 neg_probs = word_freqs ** 0.75 neg_probs = neg_probs / np.sum(neg_probs) negatives = [] while len(negatives) \u0026lt; num_negatives: neg_idx = np.random.choice(len(word_freqs), p=neg_probs) if neg_idx != target_idx and neg_idx not in negatives: negatives.append(neg_idx) return negatives 词向量的神奇运算 训练好Word2Vec后，我们得到的不只是词向量，还有一堆神奇的能力。\n相似度计算：找到词汇的\u0026quot;亲戚\u0026quot; 最基本的操作是计算两个词的相似度，用的是余弦相似度：\ndef cosine_similarity(vec1, vec2): \u0026#34;\u0026#34;\u0026#34;余弦相似度：越接近1越相似，0表示无关，-1表示相反\u0026#34;\u0026#34;\u0026#34; dot_product = np.dot(vec1, vec2) norm1 = np.linalg.norm(vec1) norm2 = np.linalg.norm(vec2) if norm1 == 0 or norm2 == 0: return 0 return dot_product / (norm1 * norm2) # 在实际使用中 similarity = cosine_similarity(model[\u0026#34;猫\u0026#34;], model[\u0026#34;狗\u0026#34;]) print(f\u0026#34;猫和狗的相似度: {similarity}\u0026#34;) # 通常会是0.8左右 找相似词：发现语义邻居 def find_similar_words(model, word, top_n=10): \u0026#34;\u0026#34;\u0026#34;找到与指定词最相似的词\u0026#34;\u0026#34;\u0026#34; target_vector = model[word] similarities = [] for other_word, other_vector in model.items(): if other_word != word: sim = cosine_similarity(target_vector, other_vector) similarities.append((other_word, sim)) # 按相似度排序，返回最相似的top_n个 similarities.sort(key=lambda x: x[1], reverse=True) return similarities[:top_n] # 试试看\u0026#34;北京\u0026#34;的相似词 similar_to_beijing = find_similar_words(model, \u0026#34;北京\u0026#34;, 5) # 可能输出：[(\u0026#34;上海\u0026#34;, 0.85), (\u0026#34;广州\u0026#34;, 0.82), (\u0026#34;深圳\u0026#34;, 0.80), ...] 最神奇的部分：词向量竟然可以做算术！ 这是Word2Vec最让人惊叹的地方：词向量可以进行加减运算，而且结果有意义！\n经典的\u0026quot;国王\u0026quot;例子 最著名的例子是：king - man + woman ≈ queen\ndef word_analogy(model, positive_words, negative_words, top_n=5): \u0026#34;\u0026#34;\u0026#34;词类比运算：A之于B，正如C之于？\u0026#34;\u0026#34;\u0026#34; # 计算目标向量 target_vector = np.zeros(model.vector_size) # 加上正样本的向量 for word in positive_words: if word in model: target_vector += model[word] # 减去负样本的向量 for word in negative_words: if word in model: target_vector -= model[word] # 归一化 target_vector = target_vector / np.linalg.norm(target_vector) # 找到最相似的词 similarities = [] for word, vector in model.items(): if word not in positive_words and word not in negative_words: similarity = cosine_similarity(target_vector, vector) similarities.append((word, similarity)) similarities.sort(key=lambda x: x[1], reverse=True) return similarities[:top_n] # 经典类比示例 result = word_analogy(model, [\u0026#34;king\u0026#34;, \u0026#34;woman\u0026#34;], [\u0026#34;man\u0026#34;]) print(\u0026#34;king - man + woman =\u0026#34;, result[0][0]) # 通常会输出 \u0026#34;queen\u0026#34; 为什么词向量可以做算术？ 这背后的逻辑是：\nking的向量包含了\u0026quot;统治者\u0026quot;+\u0026ldquo;男性\u0026quot;的语义 man的向量主要是\u0026quot;男性\u0026quot;的语义 woman的向量主要是\u0026quot;女性\u0026quot;的语义 所以 king - man + woman = \u0026ldquo;统治者\u0026rdquo; - \u0026ldquo;男性\u0026rdquo; + \u0026ldquo;女性\u0026rdquo; = \u0026ldquo;女性统治者\u0026rdquo; ≈ queen 更多有趣的类比 # 地理类比 result = word_analogy(model, [\u0026#34;北京\u0026#34;, \u0026#34;美国\u0026#34;], [\u0026#34;中国\u0026#34;]) # 北京之于中国，正如?之于美国 → 华盛顿 # 时态类比 result = word_analogy(model, [\u0026#34;walked\u0026#34;, \u0026#34;go\u0026#34;], [\u0026#34;went\u0026#34;]) # walk之于walked，正如go之于? → goes # 比较级类比 result = word_analogy(model, [\u0026#34;good\u0026#34;, \u0026#34;better\u0026#34;], [\u0026#34;bad\u0026#34;]) # good之于better，正如bad之于? → worse Word2Vec的实际应用 训练好的词向量不只是学术玩具，在实际业务中有很多用途：\n文本相似度计算 最直接的应用就是计算两段文本的相似度：\ndef document_similarity(doc1, doc2, model): \u0026#34;\u0026#34;\u0026#34;计算两个文档的相似度\u0026#34;\u0026#34;\u0026#34; # 把文档分词，获取词向量 words1 = [word for word in jieba.cut(doc1) if word in model] words2 = [word for word in jieba.cut(doc2) if word in model] if not words1 or not words2: return 0 # 文档向量 = 所有词向量的平均值 doc_vec1 = np.mean([model[word] for word in words1], axis=0) doc_vec2 = np.mean([model[word] for word in words2], axis=0) return cosine_similarity(doc_vec1, doc_vec2) # 实际使用 sim = document_similarity(\u0026#34;我喜欢吃苹果\u0026#34;, \u0026#34;我爱吃水果\u0026#34;, model) print(f\u0026#34;相似度: {sim}\u0026#34;) # 可能输出 0.85 推荐系统 电商、内容平台都能用Word2Vec做推荐：\ndef recommend_products(user_query, product_descriptions, model, top_n=5): \u0026#34;\u0026#34;\u0026#34;基于查询推荐商品\u0026#34;\u0026#34;\u0026#34; # 把用户查询转成向量 query_words = [w for w in jieba.cut(user_query) if w in model] if not query_words: return [] query_vector = np.mean([model[w] for w in query_words], axis=0) # 计算与所有商品的相似度 similarities = [] for product_id, description in product_descriptions.items(): desc_words = [w for w in jieba.cut(description) if w in model] if desc_words: desc_vector = np.mean([model[w] for w in desc_words], axis=0) sim = cosine_similarity(query_vector, desc_vector) similarities.append((product_id, sim)) # 按相似度排序 similarities.sort(key=lambda x: x[1], reverse=True) return similarities[:top_n] 聚类和分类 Word2Vec还能用来做文本聚类、分类等任务，把文档转成向量后就可以用传统的机器学习方法了。\n总结：Word2Vec的启发和思考 回过头来看，Word2Vec的成功其实给了我们很多启发：\n简单想法的巨大威力 Word2Vec的核心想法其实很朴素：相似的词出现在相似的上下文中。但就是这个简单的假设，配合神经网络的学习能力，就能捕捉到复杂的语义关系。\n这告诉我们，好的算法往往来自于对问题本质的深刻理解，而不是复杂的技巧堆砌。\n工程优化的重要性 Word2Vec的另一个成功之处是工程优化。负采样、层次softmax这些技术让训练速度提升了几千倍，这才让Word2Vec在实际应用中变得可行。\n算法的价值在于能被使用，而不是在纸面上看起来漂亮。\n影响至今的经典 虽然现在有了BERT、GPT这些更强大的模型，但Word2Vec的思想依然影响着今天的NLP发展：\n预训练词向量成了标配 语义向量空间的概念被广泛接受 负采样等训练技巧被广泛采用 最后的思考 Word2Vec告诉我们，理解语义不一定需要复杂的语法规则，有时候统计规律就足够了。这个观点在今天的大语言模型时代更是得到了验证。\n当然，Word2Vec也有局限性：它无法处理多义词（bank的银行和河岸含义），也无法捕捉长距离的依赖关系。但作为一个十年前的模型，它已经足够惊艳了。\n如果你想深入理解现代NLP，Word2Vec绝对是一个不可跳过的里程碑。它的论文不长，代码也不复杂，但思想却影响了整个领域的发展方向。\n","permalink":"https://asterzephyr.github.io/posts/word2vec_blog/","summary":"\u003ch1 id=\"word2vec深度解析让计算机理解词汇语义的黑魔法\"\u003eWord2Vec深度解析：让计算机理解词汇语义的黑魔法\u003c/h1\u003e\n\u003cp\u003e前段时间在研究一些老派的NLP模型，重新翻了翻Word2Vec的论文。虽然现在大家都在用BERT、GPT这些Transformer模型，但Word2Vec这个十年前的模型至今仍然在很多场景下发光发热。\u003c/p\u003e","title":"Word2Vec深度解析：让计算机理解词汇语义的黑魔法"},{"content":"jieba分词原理解析：一个老牌中文分词器的工程智慧 最近在做中文NLP项目的时候，又用到了jieba这个老朋友。说起jieba，大概是每个做中文处理的程序员都绕不开的工具。简单一个jieba.cut()就能把中文文本切得明明白白，但你有没有好奇过，这背后到底是怎么实现的？\n中文分词和英文不一样，英文有天然的空格分隔，而中文就是一串连续的字符。要把\u0026quot;我爱北京天安门\u0026quot;切成\u0026quot;我/爱/北京/天安门\u0026quot;，看似简单，实际上需要很多巧妙的算法设计。今天就来扒一扒jieba的实现原理。\n核心架构：模块化的设计思路 打开jieba的源码，你会发现它的结构其实挺清晰的：\njieba/ ├── 分词引擎 # 主要的切分逻辑 ├── 词典管理 # 各种词典的加载和查找 ├── 词性标注 # 给词汇打标签 ├── 关键词提取 # TF-IDF和TextRank └── 性能优化 # 缓存、并行等 jieba的分词流程也不复杂，大概分这几步：\n预处理：把文本规范化一下 词典匹配：用Trie树快速找可能的词 DAG构建：把所有可能的切分方案列出来 最优路径：用动态规划找最好的切分 后处理：处理一些边界情况 听起来挺学术的，其实每一步都有很实用的工程考虑。\n核心算法深度解析 Trie树：词典查找的效率神器 jieba用Trie树（字典树）来存词典，这个选择很聪明。为什么？因为中文分词需要大量的前缀匹配，而Trie树在这方面就是天生的好手。\n# Trie树的节点结构，其实很简单 class TrieNode: def __init__(self): self.children = {} # 子节点，key是字符，value是子节点 self.is_word = False # 标记这里是不是一个完整的词 self.frequency = 0 # 词频，用来后面算权重 假设要存储\u0026quot;北京\u0026quot;和\u0026quot;北京大学\u0026quot;，Trie树是这样的：\nroot └── 北 └── 京 [is_word=True, freq=1000] # 这里\u0026#34;北京\u0026#34;是个词 └── 大 └── 学 [is_word=True, freq=500] # 这里\u0026#34;北京大学\u0026#34;也是个词 这样设计的好处是，当我们要匹配\u0026quot;北京大学很美\u0026quot;时，可以一次遍历就找到所有可能的词边界。\n前缀匹配：找出所有可能的词 前缀匹配的逻辑就是暴力一点，从每个位置开始，看能匹配到多长：\ndef find_prefix_matches(text, trie): \u0026#34;\u0026#34;\u0026#34;找出文本中所有可能的词，返回(起始位置, 结束位置, 词)\u0026#34;\u0026#34;\u0026#34; matches = [] for i in range(len(text)): node = trie.root for j in range(i, len(text)): char = text[j] if char not in node.children: break # 这条路走不通了 node = node.children[char] if node.is_word: # 找到一个词！ matches.append((i, j+1, text[i:j+1])) return matches 比如对\u0026quot;北京大学很美\u0026quot;，这个函数会返回：\n(0, 2, \u0026ldquo;北京\u0026rdquo;) (0, 4, \u0026ldquo;北京大学\u0026rdquo;) (2, 4, \u0026ldquo;大学\u0026rdquo;) \u0026hellip; 现在我们有了所有可能的词，但问题来了：怎么从这么多种切分方案中选出最好的？\nDAG + 动态规划：选出最优切分 这里jieba用了一个很经典的思路：把问题转换成图论问题。\n构建DAG（有向无环图） 把所有可能的词当作图的边，位置当作节点：\ndef build_dag(text, matches): \u0026#34;\u0026#34;\u0026#34;基于匹配结果构建DAG\u0026#34;\u0026#34;\u0026#34; dag = [[] for _ in range(len(text) + 1)] for start, end, word in matches: dag[start].append((end, word)) # 从start到end有一条边，权重是word的得分 return dag 对\u0026quot;北京大学\u0026quot;，DAG大概是这样：\n0 --北京--\u0026gt; 2 --大学--\u0026gt; 4 0 ----北京大学-----\u0026gt; 4 现在问题变成了：从位置0到位置4，走哪条路径得分最高？\n动态规划找最优路径 这就是经典的动态规划了，思路是：到达每个位置的最优分数 = 前面某个位置的最优分数 + 这条边的得分。\ndef viterbi_segmentation(dag, text): \u0026#34;\u0026#34;\u0026#34;动态规划求最优分词路径\u0026#34;\u0026#34;\u0026#34; n = len(text) dp = [float(\u0026#39;-inf\u0026#39;)] * (n + 1) # dp[i]表示到位置i的最优得分 path = [0] * (n + 1) # 记录路径，用于回溯 dp[0] = 0 # 起点得分为0 # 动态规划过程 for i in range(n + 1): if dp[i] == float(\u0026#39;-inf\u0026#39;): continue # 这个位置不可达 for end, word in dag[i]: score = dp[i] + get_word_score(word) # 词频越高得分越高 if score \u0026gt; dp[end]: dp[end] = score path[end] = i # 回溯构建结果 result = [] pos = n while pos \u0026gt; 0: prev_pos = path[pos] result.append(text[prev_pos:pos]) pos = prev_pos return result[::-1] # 逆序得到正确顺序 get_word_score函数一般基于词频计算，常见词得分高，生僻词得分低。这样就能保证切分结果比较符合常识。\njieba的三种分词模式 jieba提供了三种不同的分词模式，应对不同的使用场景。\n精确模式：日常使用的默认选择 list(jieba.cut(\u0026#34;我来到北京清华大学\u0026#34;)) # 输出: [\u0026#39;我\u0026#39;, \u0026#39;来到\u0026#39;, \u0026#39;北京\u0026#39;, \u0026#39;清华大学\u0026#39;] 这就是我们刚才分析的算法，通过动态规划找最优路径。优点是结果质量高，缺点是可能会漏掉一些有用的分词信息。\n适用场景：日常的文本处理，比如搜索、推荐等\n全模式：把所有可能的词都找出来 list(jieba.cut(\u0026#34;我来到北京清华大学\u0026#34;, cut_all=True)) # 输出: [\u0026#39;我\u0026#39;, \u0026#39;来到\u0026#39;, \u0026#39;北京\u0026#39;, \u0026#39;清华\u0026#39;, \u0026#39;清华大学\u0026#39;, \u0026#39;华大\u0026#39;, \u0026#39;大学\u0026#39;] 全模式就是把所有能识别的词都输出，不管是否合理。这个模式计算量大（可能是指数级的），结果也冗余，但在某些场景下很有用。\n适用场景：关键词提取的预处理、需要覆盖所有可能词汇的场合\n搜索引擎模式：兼顾精确和召回 list(jieba.cut_for_search(\u0026#34;小明硕士毕业于中国科学院计算所\u0026#34;)) # 输出: [\u0026#39;小明\u0026#39;, \u0026#39;硕士\u0026#39;, \u0026#39;毕业\u0026#39;, \u0026#39;于\u0026#39;, \u0026#39;中国\u0026#39;, \u0026#39;科学\u0026#39;, \u0026#39;学院\u0026#39;, \u0026#39;科学院\u0026#39;, \u0026#39;中国科学院\u0026#39;, \u0026#39;计算\u0026#39;, \u0026#39;计算所\u0026#39;] 这个模式很聪明，先用精确模式切分，然后对长词再细分。这样既保持了基本的分词质量，又提供了更多的检索可能性。\n比如\u0026quot;中国科学院\u0026quot;既保留了完整的机构名，又拆分出了\u0026quot;中国\u0026quot;、\u0026ldquo;科学院\u0026quot;等子词，这样用户搜索任何一个都能命中。\n适用场景：搜索引擎、信息检索系统\n词性标注：给每个词打标签 除了分词，jieba还能进行词性标注，告诉你每个词是名词、动词还是形容词。这用的是HMM（隐马尔可夫模型）。\nimport jieba.posseg as pseg words = pseg.cut(\u0026#34;我爱自然语言处理\u0026#34;) for word, flag in words: print(f\u0026#34;{word} - {flag}\u0026#34;) # 输出: # 我 - r (代词) # 爱 - v (动词) # 自然语言 - l (习语) # 处理 - v (动词) HMM的基本思路是：给定前面的词性，预测当前词的词性。这需要三个概率表：\n初始概率：句子开头是某个词性的概率 转移概率：从一个词性转到另一个词性的概率 发射概率：某个词性下出现特定词汇的概率 然后用动态规划（又是它！）找出概率最大的词性序列。\n这里的实现又是Viterbi算法，跟分词时用的动态规划思路一样：\ndef hmm_tagging(words, hmm_model): \u0026#34;\u0026#34;\u0026#34;用HMM给词序列标注词性\u0026#34;\u0026#34;\u0026#34; n = len(words) tags = list(hmm_model.emission_probs.keys()) # dp[t][i]表示第t个词标注为第i个词性的最大概率 dp = [[0] * len(tags) for _ in range(n)] path = [[0] * len(tags) for _ in range(n)] # 记录路径 # 初始化第一个词 for i, tag in enumerate(tags): dp[0][i] = hmm_model.initial_probs.get(tag, 0) * \\ hmm_model.emission_probs.get(tag, {}).get(words[0], 0) # 动态规划填表 for t in range(1, n): for i, current_tag in enumerate(tags): max_prob = 0 best_prev = 0 # 枚举前一个词的所有可能词性 for j, prev_tag in enumerate(tags): # 概率 = 前面的最优概率 × 转移概率 × 发射概率 prob = dp[t-1][j] * \\ hmm_model.transition_probs.get(prev_tag, {}).get(current_tag, 0) * \\ hmm_model.emission_probs.get(current_tag, {}).get(words[t], 0) if prob \u0026gt; max_prob: max_prob = prob best_prev = j dp[t][i] = max_prob path[t][i] = best_prev # 回溯得到最优词性序列 best_path = [] current_tag = max(range(len(tags)), key=lambda i: dp[n-1][i]) for t in range(n-1, -1, -1): best_path.append(tags[current_tag]) current_tag = path[t][current_tag] return best_path[::-1] 看出来了吗？不管是分词还是词性标注，jieba都很喜欢用动态规划这个万能工具。\n关键词提取：找出文档的核心概念 jieba不只是分词，还能帮你从一篇文档中提取关键词。主要有两种算法：\nTF-IDF：经典的统计方法 TF-IDF的思路很直观：一个词在当前文档中出现频率高，但在所有文档中出现频率低，那它就很可能是关键词。\nimport jieba.analyse text = \u0026#34;程序员小明在北京的互联网公司工作，每天都要写Python代码\u0026#34; keywords = jieba.analyse.extract_tags(text, topK=5) print(keywords) # 输出: [\u0026#39;Python\u0026#39;, \u0026#39;程序员\u0026#39;, \u0026#39;小明\u0026#39;, \u0026#39;代码\u0026#39;, \u0026#39;互联网\u0026#39;] 实现起来就是经典的TF-IDF公式：\ndef tfidf_keywords(text, top_k=10): \u0026#34;\u0026#34;\u0026#34;TF-IDF关键词提取\u0026#34;\u0026#34;\u0026#34; word_freq = {} words = list(jieba.cut(text)) total_words = len(words) # 计算词频(TF) for word in words: if len(word.strip()) \u0026gt; 1: # 过滤标点和单字 word_freq[word] = word_freq.get(word, 0) + 1 # 计算TF-IDF得分 tfidf_scores = {} for word, freq in word_freq.items(): tf = freq / total_words idf = get_idf_score(word) # 从预训练的IDF表中获取 tfidf_scores[word] = tf * idf # 返回得分最高的top_k个词 return sorted(tfidf_scores.items(), key=lambda x: x[1], reverse=True)[:top_k] TextRank：把PageRank用到文本上 TextRank借鉴了Google的PageRank思想：把词汇看作网页，词与词之间的共现关系看作链接。如果一个词经常和其他重要的词一起出现，那它自己也很重要。\nkeywords = jieba.analyse.textrank(text, topK=5) print(keywords) # 输出可能和TF-IDF不同，因为考虑的是词之间的关系 TextRank的核心是构建一个词汇共现图：\ndef textrank_keywords(text, top_k=10, window_size=4): \u0026#34;\u0026#34;\u0026#34;TextRank关键词提取\u0026#34;\u0026#34;\u0026#34; words = [w for w in jieba.cut(text) if len(w.strip()) \u0026gt; 1] # 构建共现图：在窗口内一起出现的词有边相连 graph = {} for i, word in enumerate(words): if word not in graph: graph[word] = {} # 看看这个词的前后window_size个词 start = max(0, i - window_size) end = min(len(words), i + window_size + 1) for j in range(start, end): if i != j: neighbor = words[j] if neighbor not in graph[word]: graph[word][neighbor] = 0 graph[word][neighbor] += 1 # 共现次数作为边权重 # 运行PageRank算法 scores = pagerank_iteration(graph) return sorted(scores.items(), key=lambda x: x[1], reverse=True)[:top_k] def pagerank_iteration(graph, max_iter=100, damping=0.85): \u0026#34;\u0026#34;\u0026#34;迭代计算PageRank得分\u0026#34;\u0026#34;\u0026#34; scores = {word: 1.0 for word in graph} for _ in range(max_iter): new_scores = {} for word in graph: # PageRank公式：(1-d)/N + d * Σ(PR(neighbor)/L(neighbor)) new_score = (1 - damping) / len(graph) for neighbor, weight in graph[word].items(): if neighbor in scores: # 邻居的贡献 = 邻居的得分 / 邻居的出度 neighbor_out_degree = len(graph[neighbor]) new_score += damping * scores[neighbor] / neighbor_out_degree new_scores[word] = new_score scores = new_scores return scores 相比TF-IDF，TextRank更能发现文档中的潜在主题词，因为它考虑了词与词之间的语义关系。\n性能优化：工程实践的智慧 jieba不只是算法牛，工程优化也做得很到位。\n并行处理：多核时代的必然选择 当你要处理大量文本时，jieba支持多进程并行：\nimport jieba jieba.enable_parallel(4) # 开启4进程并行 # 现在jieba.cut()会自动并行处理 # 也可以手动并行处理大批量文本 from multiprocessing import Pool def batch_segment(texts): with Pool(4) as pool: results = pool.map(lambda text: list(jieba.cut(text)), texts) return results 缓存机制：避免重复计算 jieba内部实现了多层缓存：\n# jieba内部的缓存逻辑大概是这样的 class SegmentationCache: def __init__(self, max_size=10000): self.cache = {} self.max_size = max_size self.access_count = {} def get(self, text): if text in self.cache: self.access_count[text] += 1 return self.cache[text] return None def put(self, text, result): if len(self.cache) \u0026gt;= self.max_size: # LFU淘汰策略：删除访问次数最少的 least_used = min(self.access_count.items(), key=lambda x: x[1])[0] del self.cache[least_used] del self.access_count[least_used] self.cache[text] = result self.access_count[text] = 1 对于相同的文本，第二次分词几乎是瞬间完成的。\n词典优化：让查找更快 jieba还在词典存储上下了不少功夫：\n分层词典：把常用词和生僻词分开存储 前缀索引：快速定位可能的匹配 词频缓存：避免重复的词频计算 这些优化让jieba在处理大规模文本时依然保持高效。\n实际应用：把jieba用起来 在实际项目中，jieba通常不是单独使用，而是作为文本处理流水线的一部分：\nimport jieba import jieba.posseg as pseg import jieba.analyse class TextProcessor: def process_article(self, text): \u0026#34;\u0026#34;\u0026#34;一站式文本处理\u0026#34;\u0026#34;\u0026#34; # 分词 words = list(jieba.cut(text)) # 词性标注 pos_tags = list(pseg.cut(text)) # 关键词提取 keywords_tfidf = jieba.analyse.extract_tags(text, topK=10) keywords_textrank = jieba.analyse.textrank(text, topK=10) # 实体识别(基于词性) entities = self.extract_entities(pos_tags) return { \u0026#39;words\u0026#39;: words, \u0026#39;pos_tags\u0026#39;: pos_tags, \u0026#39;keywords_tfidf\u0026#39;: keywords_tfidf, \u0026#39;keywords_textrank\u0026#39;: keywords_textrank, \u0026#39;entities\u0026#39;: entities } def extract_entities(self, pos_tags): \u0026#34;\u0026#34;\u0026#34;从词性标注中提取命名实体\u0026#34;\u0026#34;\u0026#34; entities = {\u0026#39;persons\u0026#39;: [], \u0026#39;locations\u0026#39;: [], \u0026#39;organizations\u0026#39;: []} for word, pos in pos_tags: if pos == \u0026#39;nr\u0026#39;: # 人名 entities[\u0026#39;persons\u0026#39;].append(word) elif pos == \u0026#39;ns\u0026#39;: # 地名 entities[\u0026#39;locations\u0026#39;].append(word) elif pos == \u0026#39;nt\u0026#39;: # 机构名 entities[\u0026#39;organizations\u0026#39;].append(word) return entities 这样一个简单的处理器就能从原始文本中提取出丰富的结构化信息，为后续的搜索、推荐、分析等任务提供基础。\n总结：jieba给我们的启发 通过深入分析jieba的实现，我觉得有几个点特别值得学习：\n算法选择的智慧 jieba没有追求最新最炫的算法，而是选择了最合适的：\nTrie树：查找效率高，实现简单 动态规划：既用于分词又用于词性标注，一招鲜吃遍天 传统统计方法：TF-IDF和TextRank至今依然好用 工程优化的重要性 光有好算法还不够，jieba在工程实现上的优化同样出色：\n缓存机制避免重复计算 并行处理充分利用多核CPU 分层存储提高查找效率 接口设计的用户友好 import jieba result = jieba.cut(\u0026#34;我爱自然语言处理\u0026#34;) # 就这么简单 简单的API背后是复杂的算法实现，但用户不需要关心。这种设计理念值得所有开发者学习。\n最后的感想 jieba虽然诞生于深度学习兴起之前，但到现在依然是中文NLP的主力工具。这说明了什么？技术的价值不在于新，而在于解决实际问题。\n当然，现在有了BERT、ChatGPT这些更强大的模型，但在很多场景下，jieba依然是性价比最高的选择。毕竟，不是所有的钉子都需要用大锤来敲。\n如果你对NLP有兴趣，建议去读读jieba的源码。虽然只有几千行Python代码，但里面的算法思想和工程实践都很值得学习。\n","permalink":"https://asterzephyr.github.io/posts/jieba_blog/","summary":"\u003ch1 id=\"jieba分词原理解析一个老牌中文分词器的工程智慧\"\u003ejieba分词原理解析：一个老牌中文分词器的工程智慧\u003c/h1\u003e\n\u003cp\u003e最近在做中文NLP项目的时候，又用到了jieba这个老朋友。说起jieba，大概是每个做中文处理的程序员都绕不开的工具。简单一个\u003ccode\u003ejieba.cut()\u003c/code\u003e就能把中文文本切得明明白白，但你有没有好奇过，这背后到底是怎么实现的？\u003c/p\u003e","title":"jieba分词原理解析：一个老牌中文分词器的工程智慧"},{"content":"为什么延迟双删在生产环境中很少使用？真实的缓存一致性策略 提到缓存，一个绕不开的话题就是缓存与数据库的一致性。\n在学习缓存理论时，我对“延迟双删”这个精巧的设计印象深刻。它通过“删除缓存 -\u0026gt; 更新数据库 -\u0026gt; 延迟再次删除缓存”这三步，似乎完美地解决了“脏数据”问题。\n然而，一个残酷的现实是：在我分析过的大量真实、高并发的生产代码中，这个理论上的“优等生”却鲜有出场机会。\n为什么？在真实业务的严苛要求下，我们究竟是如何保证缓存一致性的？这篇文章将通过一个我们真实的广告竞价（ADX）系统案例，带你深入了解那些在生产环境中真正被信赖和广泛使用的缓存策略。\n案例研究：高性能广告竞价系统（ADX）的缓存设计 广告竞价是一个对性能和时效性要求到极致的场景。一次竞价请求必须在几十毫秒内完成，数据每时每刻都在高频变化。让我们看看这样的系统是如何设计缓存的。\n核心策略一：Cache Aside + 读后即删 该系统的核心出价缓存（BidCache）并未使用复杂的更新策略，而是采用了极其简洁高效的 Cache Aside Pattern，并附带了一个特殊操作：读后即删。\n读操作：应用先从 Redis 缓存中读取出价数据。 缓存命中：如果命中，立即从缓存中删除该条目，然后返回数据。 缓存未命中：查询后端数据库或服务，获取出价数据后返回给应用（通常不回写到缓存，因为一次竞价的响应是唯一的）。 // 广告竞价中，一个出价响应只能被使用一次 // 因此采用“读后即删”策略，确保数据的一次性消费 // 出价缓存的Pop操作 func (bc *BidCache) Pop(ctx context.Context, dspId int, req *adx.RTBAdsRequest) []byte { identity := generateRequestIdentity(req) // 根据请求生成唯一标识 // 1. 先从Redis GET数据 resp, err := bc.rdb.Get(ctx, identity).Bytes() if err != nil { return nil // 缓存未命中 } // 2. 命中后立即删除，避免重复使用 bc.rdb.Del(ctx, identity).Err() return resp } 为什么这么设计？\n业务特点决定：广告出价响应是一次性的，用完即作废，读后即删完美契合。 性能优先：没有任何多余的写操作或延迟等待，最大化读写性能。 天然一致：数据用完就删，不存在“脏数据”污染后续请求的可能。 核心策略二：多层缓存架构 + TTL 自动过期 除了核心的出价缓存，系统还广泛使用了分层和TTL机制：\n多层缓存：使用多个专用的 Redis 分布式缓存实例（主缓存、用户标签、频次控制、算法模型等），并配合进程内的 fastcache + sync.Map 作为热点数据的L1缓存。 TTL 自动过期：几乎所有的缓存都设置了较短的TTL（从秒级到分钟级）。依赖 Redis 的自动过期机制来清理数据，这是保证最终一致性、防止垃圾数据堆积的最简单可靠的方式。 在这个场景下，“延迟双删”不仅毫无用武之地，反而会因为引入不必要的延迟和复杂度而成为性能瓶颈。\n延迟双删的“不舒适区”：为什么我们在生产中很少用它？ 通过上面的案例，我们不难发现，好的架构总是与业务场景深度绑定。延迟双删作为一个“理论完美”的方案，在现实中却面临着诸多挑战。\n1. 复杂度与收益不匹配 为了解决一个在绝大多数场景下极小概率发生的“读写并发”问题，引入一个需要额外维护延迟任务的复杂机制，这在工程上往往是得不偿失的。\n// 理论上的延迟双删，在生产中引入了诸多问题 func updateUser(userID int, data UserData) error { // 1. 删除缓存 cache.Delete(\u0026#34;user:\u0026#34; + userID) // 2. 更新数据库 db.Update(userID, data) // 3. 延迟再次删除缓存 - 噩梦的开始 time.AfterFunc(500*time.Millisecond, func() { // - 这个goroutine如果panic了怎么办？ // - 服务在延迟期间重启了怎么办？ // - 如果业务逻辑需要重试，这个延迟任务如何保证幂等？ cache.Delete(\u0026#34;user:\u0026#34; + userID) }) return nil } 2. “延迟多久”是个玄学问题 延迟500毫秒？1秒？这个时间需要大于数据库主从同步的延迟。但在企业级别复杂的分布式环境中，网络抖动、数据库负载都可能导致这个延迟时间变得不可预测。依赖一个不确定的“魔法数字”来保证确定性，是架构设计的大忌。\n3. 更好的替代方案层出不穷 工程的本质是权衡（Trade-off）。现实中，我们有更多、更简单、更可靠的武器库来应对不同场景的一致性需求。\n生产环境的设计：真实、可靠的一致性策略 下面，让我们看看在电商、金融、内容等核心业务中，那些经受了真实流量考验的缓存一致性策略。\n策略一：简单删除 + TTL过期（90%场景的首选） 这是最常见、最简单的策略，也被称为 Cache Aside (Write-Invalidate)。\n流程：先更新数据库，再直接删除缓存。 优点：简单、高效、可靠。 缺点：理论上，在极端的并发情况下（更新DB后，删除缓存前，有另一个读请求穿透到DB读了旧数据并写回缓存），可能导致短暂数据不一致。 适用场景：绝大多数能容忍秒级数据不一致的场景。比如用户信息、商品介绍、文章内容等。因为这个并发窗口期极短，且即便发生，TTL也会在短时间内纠正数据。 // 1. 适用于一致性要求不高的场景 func updateUserProfile(userID string, profile UserProfile) error { // 先更新数据库 if err := db.Update(userID, profile); err != nil { return err } // 再简单删除缓存，让其通过懒加载或TTL自然过期重建 cache.Delete(\u0026#34;user_profile:\u0026#34; + userID) return nil } 策略二：阿里的Canal + MQ 异步通知 对于微服务架构，或者需要对缓存更新进行精细化控制的场景，基于数据库binlog的异步通知是最佳实践。\n流程：应用只管更新数据库 -\u0026gt; Debezium/Canal 订阅数据库 binlog -\u0026gt; 将数据变更消息发送到 MQ（Kafka/RocketMQ）-\u0026gt; 一个专门的缓存同步服务消费消息，并对缓存进行精准的更新或删除。 优点：应用与缓存管理完全解耦、高可用（MQ保证消息不丢失）、可追溯、性能影响小。 适用场景：需要保证最终一致性、系统间交互复杂、流量巨大的核心业务。如电商系统的商品信息同步。 策略三：版本号机制（应对“配置类”数据的利器） 当更新的数据是配置、规则等重要信息时，我们不希望出现新旧版本数据混杂的情况。\n流程：在缓存的数据中增加一个版本号字段（或直接用时间戳）。每次更新数据库时，版本号递增。应用读取缓存时，可以校验版本号；或者在更新缓存时，直接用新版本数据覆盖旧版本。 优点：能有效防止旧数据被错误地写回缓存，控制更精确。 适用场景：金融风控规则、系统配置参数、AB实验策略等。 // 3. 适用于配置类数据 func updateConfig(key string, value interface{}) error { // 使用时间戳作为版本号 config := \u0026amp;Config{ Key: key, Value: value, Version: time.Now().UnixNano(), } // 更新DB后，将带版本号的完整数据写入缓存 db.Save(config) cache.Set(\u0026#34;config:\u0026#34;+key, config, 30*time.Minute) return nil } 策略四：不缓存（终极一致性方案） 当遇到像金融余额、电商库存这类绝对不能出现不一致的数据时，最简单、最安全的策略就是：放弃缓存。\n流程：所有的读、写操作，全部直接穿透到数据库，并利用数据库事务来保证其原子性和一致性。 优点：强一致性。 适用场景：金融级核心数据、库存管理等对数据精确性要求100%的场景。 // 2. 适用于一致性要求极高的场景 func updateUserBalance(userID string, amount int64) error { // 直接操作数据库，不走任何缓存，并使用事务保证原子性 return db.Transaction(func(tx *Tx) error { return tx.UpdateBalance(userID, amount) }) } 结论：务实胜于完美 回到最初的问题：为什么“延迟双删”在真实业务中很少见？\n答案是：因为它试图用一种复杂的手段，去解决一个在大部分场景下并不严重、且有更多简单可靠方案可以替代的问题。\n在真实的工程世界里，我们永远在做权衡。简单可靠、易于维护、能满足业务需求的方案，永远优于那个理论上完美无瑕但实施起来却举步维艰的\u0026quot;银弹\u0026quot;。\n","permalink":"https://asterzephyr.github.io/posts/double-delete/","summary":"\u003ch1 id=\"为什么延迟双删在生产环境中很少使用真实的缓存一致性策略\"\u003e为什么延迟双删在生产环境中很少使用？真实的缓存一致性策略\u003c/h1\u003e\n\u003cp\u003e提到缓存，一个绕不开的话题就是\u003cstrong\u003e缓存与数据库的一致性\u003c/strong\u003e。\u003c/p\u003e\n\u003cp\u003e在学习缓存理论时，我对“\u003cstrong\u003e延迟双删\u003c/strong\u003e”这个精巧的设计印象深刻。它通过“删除缓存 -\u0026gt; 更新数据库 -\u0026gt; 延迟再次删除缓存”这三步，似乎完美地解决了“脏数据”问题。\u003c/p\u003e","title":"为什么延迟双删在生产环境中很少使用？真实的缓存一致性策略"},{"content":"Do the right thing, wait to get fired New Google employees (we call \u0026ldquo;Nooglers\u0026rdquo;) often ask me what makes me effective at what I do. I tell them only half-jokingly that it\u0026rsquo;s very simple: I do the Right Thing for Google and the world, and then I sit back and wait to get fired. If I don\u0026rsquo;t get fired, I\u0026rsquo;ve done the Right Thing for everyone. If I do get fired, this is the wrong employer to work for in the first place. So, either way, I win. That is my career strategy.\n新谷歌员工（我们称之为\u0026quot;新新员工\u0026quot;）经常问我是什么让我在所做事情上如此有效。我半开玩笑地告诉他们这很简单：我为谷歌和世界做正确的事，然后坐下来等着被解雇。如果我没有被解雇，我就为所有人做了正确的事。如果我确实被解雇了，那本来就不是适合我的雇主。所以，无论哪种情况，我都赢了。这就是我的职业策略。\nI discovered where I got this rebel streak from only very recently. I realized I inherited it from my dad, which was very strange to me because when I was growing up, I perceived my dad as an establishment figure, part of the very establishment I was rebelling against, so it was a severe cognitive dissonance for me to think of my dad as a rebel. But rebel he was.\n我最近才意识到自己这种叛逆性格的来源。我意识到这是从我父亲那里继承来的，这让我觉得很奇怪，因为我小时候认为我父亲是一个体制内的人物，正是我反叛的对象之一，所以把我父亲看作一个反叛者让我产生了严重的认知失调。但事实就是如此，他确实是个反叛者。\nMy dad started his career as a child laborer (yes, one of those millions of faceless children in developing countries you read about occasionally on National Geographic), but by mid-career, he rose up the ranks to become one of the most senior military officers in all of Singapore. I recently learned that one reason he was so successful was because he was unafraid to speak the unpleasant truth to his superiors to their faces, including Defense Ministers and Prime Ministers. Near the end of his military career, one of his superiors asked him what made him so effective. My father replied, \u0026ldquo;It\u0026rsquo;s very simple. Everyday on my drive home, I would pass by HDB flats (public housing in Singapore) and I would always take an extra look at them. Why? Because after you fire me, that is where I\u0026rsquo;d live.\u0026rdquo;\n我的父亲起初是一名童工（是的，就是那些你在《国家地理》杂志偶尔读到的、发展中国家里无数无名的孩子之一），但在职业生涯中期，他晋升为新加坡最高级的军官之一。我最近了解到，他如此成功的一个原因是他不害怕向上级当面说出令人不快的真相，包括国防部长和总理。在他军事生涯接近尾声时，一位上级问他是什么让他如此有效。我父亲回答说：\u0026ldquo;很简单。每天开车回家时，我都会经过组屋（新加坡的公共住房），我总是会多看它们一眼。为什么？因为在你解雇我之后，那就是我要住的地方。\u0026rdquo;\nThat was his way of saying he was not afraid to be fired for doing the right thing, because he had mentally prepared himself for that possibility. It freed him to do the right thing regardless of the consequences to his career.\n这是他表达不害怕因为做正确的事而被解雇的方式，因为他已经在心理上为这种可能性做好了准备。这让他能够不顾对职业生涯的后果而做正确的事。\nHis fearlessness made him extremely valuable to Singapore, and that was a very important reason why he rose to the very top. That is the paradox of career success: the less you cling to your job, the more successful you are likely to become. It is the attachment to employment that makes you less employable, and the willingness to lose your job that makes you more valuable. The best way to take care of your future is to take care of your present. If you do the right thing now, the future will take care of itself.\n他的无畏让他对新加坡来说极其宝贵，这也是他能够升到最高职位的一个非常重要的原因。这就是职业成功的悖论：你越不依恋你的工作，你就越有可能成功。对就业的依恋让你变得不那么受雇，而愿意失去工作的态度让你变得更有价值。照顾好未来的最好方法就是照顾好现在。如果你现在做正确的事，未来会自己照顾自己。\nThis simple philosophy guides every decision I make in my career. I do not optimize for my performance review, or my relationship with my boss, or my likelihood of promotion, or my job security. I optimize for what is right. The funny thing is, it has worked out well for my performance review, and my relationship with my boss, and my likelihood of promotion, and my job security. It seems that when your true intent is to do right by everyone (including your company), somehow it eventually comes back to benefit you too.\n这个简单的哲学指导着我职业生涯中的每一个决定。我不为绩效考核、与老板的关系、晋升可能性或工作安全而优化。我为正确的事情而优化。有趣的是，这对我的绩效考核、与老板的关系、晋升可能性和工作安全都产生了积极影响。看起来当你的真正意图是为每个人（包括你的公司）做正确的事时，不知何故它最终也会让你受益。\nThat is my career strategy. Do the right thing, wait to get fired. If you don\u0026rsquo;t get fired, you win. If you get fired, you also win. Either way, you win.\n这就是我的职业策略。做正确的事，等着被解雇。如果你没有被解雇，你就赢了。如果你被解雇了，你也赢了。无论哪种情况，你都赢了。\n","permalink":"https://asterzephyr.github.io/posts/rightthing/","summary":"\u003ch1 id=\"do-the-right-thing-wait-to-get-fired\"\u003eDo the right thing, wait to get fired\u003c/h1\u003e\n\u003cp\u003e\u003cstrong\u003eNew Google employees (we call \u0026ldquo;Nooglers\u0026rdquo;) often ask me what makes me effective at what I do.  I tell them only half-jokingly that it\u0026rsquo;s very simple: I do the Right Thing for Google and the world, and then I sit back and wait to get fired.  If I don\u0026rsquo;t get fired, I\u0026rsquo;ve done the Right Thing for everyone.  If I do get fired, this is the wrong employer to work for in the first place.  So, either way, I win.  That is my career strategy.\u003c/strong\u003e\u003c/p\u003e","title":"Do the right thing, wait to get fired"},{"content":"图计算增强反欺诈风控 引言：当传统\u0026quot;规则引擎\u0026quot;遭遇瓶颈 想象一个典型的信用卡盗刷场景：一个欺诈团伙通过各种手段获取了大量用户信用卡信息，他们并不直接盗刷，而是通过一系列精心设计的、看似无关的交易，在一级、二级甚至三级商户网络中快速转移资金并最终套现。\n对于传统的反欺诈系统，这几乎是一场噩梦。这些系统大多依赖于规则引擎（Rule Engine），通过一系列 IF-THEN 逻辑来判断风险。这种方法的局限性日益凸显：\n规则静态：欺诈手段层出不穷，静态规则库永远在\u0026quot;亡羊补牢\u0026quot;。 数据孤岛：规则引擎审查的是孤立的个人或单次交易，无法看到隐藏在数据背后的\u0026quot;关系网\u0026quot;。 易于规避：专业的欺诈团伙早已摸透了这些规则，他们会将大额交易拆分成多笔小额，或利用多个\u0026quot;干净\u0026quot;的中间账户进行跳转，完美绕过监控。 问题的核心在于，我们面对的敌人早已不是\u0026quot;个体作案\u0026quot;，而是高度组织化、网络化的\u0026quot;团伙作案\u0026quot;。要战胜他们，我们的技术思维必须从关注**\u0026ldquo;规则\u0026rdquo;，升级到洞察\u0026ldquo;关系\u0026rdquo;**。而这，正是图计算技术大放异彩的舞台。\n第一部分：图之范式——重塑风险认知的新视角 图计算的核心思想是将世界建模成一个由\u0026quot;点\u0026quot;和\u0026quot;边\u0026quot;构成的网络。\n在反欺诈场景中：\n节点（Vertex）：用户、设备、IP地址、银行卡、商户等实体 边（Edge）：转账、登录、绑定、交易等行为关系 通过这种方式，原本散落在数据库表中的孤立数据点，被编织成了一张巨大且信息丰富的关系网络。欺诈团伙在其中留下的蛛丝马迹，不再是零散的日志，而是结构化的、可被分析的图谱模式。\n第二部分：核心方法论——从离线挖掘到实时拦截的飞跃 图计算在反欺诈领域的实践，经历了从离线分析到实时拦截的演进。这两种模式相辅相成，构成了现代风控的纵深防御体系。\n2.1 基础：基于批量计算的离线图挖掘 这是图反欺诈的 foundational step，旨在通过对海量历史数据的深度分析，挖掘出潜藏的、宏观的欺诈模式和团伙。\n第一步：构建带权重的风险图\n图中的\u0026quot;边\u0026quot;并非生而平等。一个母亲给儿子的常规转账，和两个陌生账户之间深夜的快速大额交易，其风险含义截然不同。因此，我们为\u0026quot;边\u0026quot;赋予权重（Weight），也即风险评分。有趣的是，我们并未完全抛弃\u0026quot;风控规则\u0026quot;，而是将其巧妙地\u0026quot;降维使用\u0026quot;——用风控规则来动态计算边的权重。\n\u0026ldquo;风控规则其实是用来算权重的。\u0026rdquo;\n举个例子： 用户张三，其信用卡评分较低，每月正常还款额约为400元。现在，他突然向一个陌生账户转账了1万元。我们可以设计一个权重计算函数：\n风险权重公式 (示例) 边权重 = f(交易金额, 用户历史行为, 对方账户风险, 时间特征, 设备特征, ...) 具体实现： W(A→B) = α₁ × 金额异常度 + α₂ × 关系陌生度 + α₃ × 时间异常度 + α₄ × 设备风险度 这条边的权重（风险分）会变得非常高。通过这种方式，我们将业务逻辑量化为图上的拓扑属性，构建了一张带权重的有向图（Weighted Directed Graph）。\n第二步：用图算法挖掘\u0026quot;欺诈孤岛\u0026quot;\n在T+1的批处理窗口，我们可以对这张全量快照图运行复杂的算法，来定位欺诈团伙。\n极大连通子图 (Maximal Connected Subgraph)：用于圈定所有可能相关的实体，锁定一个大的嫌疑范围。 最小割 (Minimum Cut)——精准\u0026quot;切割\u0026quot;出犯罪团伙：一个成熟的欺诈网络，其内部关联必然是紧密而频繁的，而他们与外部正常用户网络的关联则是稀疏而薄弱的。最小割算法的目标，就是找到并\u0026quot;切断\u0026quot;这些最薄弱的连接边，将一张庞大、混沌的图，精准地切割成数个独立的、高内聚的\u0026quot;欺诈孤岛\u0026quot;。 三角形计算 (Triangle Counting)：一个衡量社区紧密度的经典指标。一个网络中的三角形越多，代表其内部成员\u0026quot;抱团\u0026quot;越紧，是典型的高风险团伙特征。 2.2 进阶：基于流计算的实时图风控 离线挖掘能够发现宏观模式，但对于正在发生的欺诈行为，我们需要毫秒级的实时拦截能力。这就需要构建一套基于流计算的实时图风控架构。\n系统架构概览\n事件注入层 (Ingestion Layer)：Apache Kafka 等消息队列接收来自业务系统的实时事件流。 流处理/计算层 (Processing Layer)：Apache Flink 等流处理引擎作为系统大脑，订阅事件并执行计算。 图状态存储层 (Graph State Storage)：TuGraph 或其他支持高频读写的图数据库，持久化存储整张图的\u0026quot;活\u0026quot;状态。 结果缓存/服务层 (Serving Layer)：Redis 等内存数据库存放最终的实时风险评分，供下游高速查询。 应用/API层 (Application Layer)：提供低延迟API接口，供业务系统在交易中同步调用。 核心流程：一笔交易的实时风险计算之旅\n让我们跟随一笔从账户A到账户B的转账，看看它在系统中的毫秒级旅程：\n事件产生: 交易系统向 Kafka 发送一条消息。 Flink作业消费: Flink 作业消费消息，解析出 (A)-[转账]-\u0026gt;(B)。 图的局部更新: Flink 程序向图数据库发起请求，创建这条新的边并更新A和B的出入度。 触发增量计算（核心）: 定义计算范围: 设定一个范围，如2跳邻居（2-hop neighborhood），避免计算在全图蔓延。 提取局部子图: Flink 从图数据库中，拉取A、B及其一、二度邻居的属性到内存。 执行计算逻辑: 在这个局部子图上，实时计算新风险。例如，A的邻居平均风险分是多少？B是否连接了其他\u0026quot;骡子账户\u0026quot;？A和B是否属于同一个已知的欺诈社区？ 汇总新风险: 通过加权模型（可以是简单的线性归因，也可以是预训练好的GNN模型），计算出A和B的新风险总分。 结果传播与输出: 计算出的新风险分被写回图数据库，并同步到Redis缓存中，覆盖旧值。 应用调用: 业务系统通过API从Redis查询，在几毫秒内就能得到最新的风险分并做出决策。 挑战与权衡\n延迟 vs. 准确度: 增量计算的范围（跳数）越大，分析得越准，但耗时也越长，这是一个核心的业务权衡。 \u0026ldquo;热点\u0026quot;问题: 超级节点（如大型商户账户）会产生计算瓶颈，需要通过图拆分、异步处理等手段优化。 一致性: 大多数系统选择**最终一致性（Eventual Consistency）**来换取高性能，这对于反欺诈这种概率性判断场景通常可以接受。 第三部分：技术与实践 3.1 演进中的技术栈 离线分析平台: 以 NebulaGraph 为代表的分布式图数据库，拥有强大的存储和批量计算能力，非常适合T+1的深度、全局模式挖掘。 实时计算平台: 以 TuGraph 为代表的支持流图计算架构的系统，能更好地支撑我们第二部分2.2节描述的实时架构，实现准实时的风险识别。 3.2 实践一瞥：Cypher查询示例 无论后台多么复杂，分析师与图交互的语言却可以非常直观。例如，使用Cypher查询\u0026quot;与已知欺诈犯共享设备，且最近一小时内有过高风险交易的用户\u0026rdquo;：\n// 匹配已知欺诈犯、共享设备和嫌疑人 MATCH (fraudster:User {id: \u0026#39;known_fraudster_id\u0026#39;})-[:USED_DEVICE]-\u0026gt;(device:Device)\u0026lt;-[:USED_DEVICE]-(suspect:User) // 匹配嫌疑人最近一小时的交易 MATCH (suspect)-[t:TRANSACTION]-\u0026gt;() WHERE t.timestamp \u0026gt; timestamp() - 3600000 // 按总风险分汇总和排序 WITH suspect, SUM(t.risk_score) AS total_risk WHERE total_risk \u0026gt; 1000 RETURN suspect.id, total_risk ORDER BY total_risk DESC 结语 图计算技术为反欺诈风控带来了范式级的革新。它不仅仅是一种新的技术工具，更是一种全新的思维方式——从关注孤立的\u0026quot;点\u0026quot;，转向洞察复杂的\u0026quot;网\u0026quot;。\n在这个数据驱动的时代，欺诈手段日益复杂化、网络化，传统的规则引擎已经难以应对。而图计算，以其强大的关系建模能力和实时计算性能，正在成为现代风控体系的核心技术。\n从离线的团伙挖掘，到实时的风险拦截，图计算技术正在重新定义反欺诈的边界。未来，随着图神经网络（GNN）、知识图谱等技术的进一步发展，我们有理由相信，基于图计算的智能风控系统将变得更加精准、高效，为金融安全保驾护航。\n","permalink":"https://asterzephyr.github.io/posts/graph-computing-anti-fraud/","summary":"\u003ch1 id=\"图计算增强反欺诈风控\"\u003e图计算增强反欺诈风控\u003c/h1\u003e\n\u003ch2 id=\"引言当传统规则引擎遭遇瓶颈\"\u003e引言：当传统\u0026quot;规则引擎\u0026quot;遭遇瓶颈\u003c/h2\u003e\n\u003cp\u003e想象一个典型的信用卡盗刷场景：一个欺诈团伙通过各种手段获取了大量用户信用卡信息，他们并不直接盗刷，而是通过一系列精心设计的、看似无关的交易，在一级、二级甚至三级商户网络中快速转移资金并最终套现。\u003c/p\u003e","title":"图计算增强反欺诈风控"},{"content":"复杂业务模型抽象架构 几乎所有复杂的业务逻辑，都可以被拆解或组合成三种核心模型的应用：\n\u0026ldquo;做什么决策？\u0026rdquo; —— 策略引擎负责回答这个问题。 \u0026ldquo;呈现什么顺序？\u0026rdquo; —— 排序架构负责回答这个问题。 \u0026ldquo;按什么步骤做？\u0026rdquo; —— 流程引擎/任务系统负责回答这个问题。 1. 策略引擎 (Policy/Strategy Engine) 这是\u0026quot;满足了哪些条件，就可以命中哪些结果\u0026quot;的模式。它的核心是将易变的业务规则（策略）与相对稳定的系统代码分离开来。\n核心思想 定义一系列规则（条件），当输入的数据（事实）满足这些规则时，系统执行相应的动作或返回特定的结果。A/B测试、风控引擎的原理。\n典型应用场景 风控系统：用户注册、登录、交易、发帖等行为，命中不同风险等级的规则，从而触发拦截、验证码、人工审核等不同操作。 营销活动：用户满足地域、会员等级、历史消费行为等条件，即可命中优惠券、折扣、弹窗提醒等营销策略。 A/B测试与灰度发布：根据用户ID、设备信息、地理位置等，将用户分流到不同的实验组，下发不同的产品策略（UI、算法、业务逻辑等）。 访问控制（Authorization）：用户拥有什么角色、属于哪个部门，决定了他能访问哪些资源、执行哪些操作。 内容分发：根据用户画像和内容标签，决定向用户推荐哪一类内容。 涉及的关键技术和知识点 规则引擎（Rule Engine）：如 Drools、Easy Rules 等，提供规则的定义、存储、匹配和执行能力。 DSL（Domain Specific Language）：为业务人员提供易于理解和编写的规则描述语言。 Rete算法：高效的模式匹配算法，是大多数规则引擎的核心。 事实库（Fact Base）：存储当前状态和输入数据的地方。 推理引擎（Inference Engine）：负责将事实与规则进行匹配，并执行相应的动作。 高度抽象的模型 +-------------------+ 输入数据 -\u0026gt; | 特征/事实提取 | -\u0026gt; [事实集合] +-------------------+ | v +----------------+ +-----------------------+ +-------------------+ | 规则/策略仓库 | -\u0026gt; | 规 则 引 擎 | -\u0026gt; | 决策结果/动作 | | (DB, Config) | | (Rete算法, DSL解析) | | (通过, 拒绝, 发券) | +----------------+ +-----------------------+ +-------------------+ ^ | +-------------------+ | A/B实验分流模块 | +-------------------+ 2. 排序架构 (Ranking Architecture) 理解得非常准确：\u0026ldquo;算法模型给出来的结果，它不一定精确，他需要的后端工程再去手动的（干预）\u0026quot;。这完美描述了\u0026quot;搜广推\u0026quot;中的重排（Re-ranking）阶段。\n核心思想 将一个候选集合（商品、内容、广告等）按照某种评分标准进行排序，以优化用户体验或业务指标。现代排序架构通常是多层漏斗结构，从粗糙到精细，从算法到规则。\n典型应用场景 搜索引擎：对搜索结果按相关性、权威性、时效性等进行排序。 推荐系统：对推荐内容按用户兴趣、内容质量、多样性等进行排序。 广告系统：对广告按eCPM（预期收益）、CTR（点击率）、用户体验等进行排序。 电商平台：对商品按销量、评分、价格、库存等进行排序。 内容平台：对文章、视频按热度、质量、个性化匹配度等进行排序。 涉及的关键技术和知识点 多层排序架构：召回 -\u0026gt; 粗排 -\u0026gt; 精排 -\u0026gt; 重排，每一层都有不同的优化目标和计算复杂度。 机器学习排序：Learning to Rank (LTR)，如 RankNet、LambdaMART、XGBoost 等。 特征工程：用户特征、物品特征、上下文特征、交叉特征等。 在线学习：实时更新模型参数，适应用户行为变化。 多目标优化：平衡点击率、转化率、用户满意度、收益等多个指标。 重排策略： 打散（Diversification）：避免同类内容过度聚集。 去重（Deduplication）：移除重复或相似内容。 强插（Insertion）：插入特定的运营内容或广告。 规则保护：确保结果符合法规和平台政策。 高度抽象的模型 +----------+ +----------+ +----------+ | 召回层 |--\u0026gt;| 粗排层 |--\u0026gt;| 精排层 |--\u0026gt; [算法排序列表] | (多路召回) | | (轻量模型) | | (复杂模型) | +----------+ +----------+ +----------+ | v +-----------------+ | 重排层 | | (打散, 去重, 强插) | --\u0026gt; 最终呈现给用户的列表 +-----------------+ 3. 流程引擎 / 任务系统 (Workflow Engine / Task System) 对这个模型的描述也非常到位：\u0026ldquo;执行完A，再执行B\u0026hellip;延迟20分钟再执行C\u0026hellip;各种任务的编排调度\u0026rdquo;。它解决了业务流程的自动化和可靠性问题。\n核心思想 将一个复杂的端到端业务流程，拆解为一系列独立的、可编排的、可观测的任务节点（Node），并根据预设的逻辑（串行、并行、分支、延迟、事件触发）来调度和驱动整个流程的执行。\n典型应用场景 电商订单系统：下单 -\u0026gt; 锁库存 -\u0026gt; 支付 -\u0026gt; 通知仓库 -\u0026gt; 发货 -\u0026gt; 确认收货，这是一个经典的线性流程，但其中支付、发货等环节可能包含复杂的子流程和异常处理。 商品发布流程：上传图片 -\u0026gt; 图片处理（加水印、压缩）-\u0026gt; 内容审核（机审+人审） -\u0026gt; 同步到搜索引擎 -\u0026gt; 上架。 数据处理 (ETL Pipeline)：从不同数据源抽取数据 -\u0026gt; 清洗转换 -\u0026gt; 加载到数据仓库，这是一个典型的数据任务流。 用户增长任务：用户完成任务A（签到）-\u0026gt; 发放奖励 -\u0026gt; 触发任务B（去浏览商品）的引导。 SRE/运维自动化：监控到告警 -\u0026gt; 自动执行诊断脚本 -\u0026gt; 尝试自动恢复 -\u0026gt; 失败则创建工单并通知负责人。 涉及的关键技术和知识点 工作流引擎：如 Activiti、Camunda、Temporal、Airflow 等。 状态机：有限状态机（FSM）用于建模任务的生命周期和状态转换。 DAG（有向无环图）：用于表示任务间的依赖关系和执行顺序。 事件驱动架构：通过事件来触发任务的执行和状态变更。 补偿机制（Saga Pattern）：在分布式环境下，当某个步骤失败时，如何回滚之前的操作。 重试与容错：任务失败时的重试策略、熔断机制、降级处理等。 监控与可观测性：任务执行状态、耗时、成功率等指标的监控和告警。 高度抽象的模型 [事件/触发器] -\u0026gt; [流程引擎] -\u0026gt; [任务调度器] | | v v [流程定义存储] [任务队列/消息队列] | | v v [状态管理/持久化] [任务执行器集群] | | v v [监控/告警系统] [结果回调/通知] 结论 这三大模型：\n策略引擎 解决 \u0026ldquo;判断\u0026rdquo; 的问题。 排序架构 解决 \u0026ldquo;择优\u0026rdquo; 的问题。 流程引擎 解决 \u0026ldquo;执行\u0026rdquo; 的问题。 在真实的重型后端业务中，这三者往往是融合在一起的。比如：一个营销活动（策略引擎决策），可能会触发一个用户任务流（流程引擎执行），任务流的最终奖励可能是个性化的优惠券列表（排序架构生成）。\n理解和掌握这三大抽象模型，能够帮助我们更好地设计和实现复杂的业务系统，提高系统的可维护性、可扩展性和业务敏捷性。\n","permalink":"https://asterzephyr.github.io/posts/complex-business-model-architecture/","summary":"\u003ch1 id=\"复杂业务模型抽象架构\"\u003e复杂业务模型抽象架构\u003c/h1\u003e\n\u003cp\u003e几乎所有复杂的业务逻辑，都可以被拆解或组合成三种核心模型的应用：\u003c/p\u003e\n\u003col\u003e\n\u003cli\u003e\u003cstrong\u003e\u0026ldquo;做什么决策？\u0026rdquo;\u003c/strong\u003e —— \u003cstrong\u003e策略引擎\u003c/strong\u003e负责回答这个问题。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e\u0026ldquo;呈现什么顺序？\u0026rdquo;\u003c/strong\u003e —— \u003cstrong\u003e排序架构\u003c/strong\u003e负责回答这个问题。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e\u0026ldquo;按什么步骤做？\u0026rdquo;\u003c/strong\u003e —— \u003cstrong\u003e流程引擎/任务系统\u003c/strong\u003e负责回答这个问题。\u003c/li\u003e\n\u003c/ol\u003e\n\u003chr\u003e\n\u003ch2 id=\"1-策略引擎-policystrategy-engine\"\u003e1. 策略引擎 (Policy/Strategy Engine)\u003c/h2\u003e\n\u003cp\u003e这是\u0026quot;满足了哪些条件，就可以命中哪些结果\u0026quot;的模式。它的核心是\u003cstrong\u003e将易变的业务规则（策略）与相对稳定的系统代码分离开来\u003c/strong\u003e。\u003c/p\u003e","title":"复杂业务模型抽象架构"},{"content":"TCP粘包：一个经典“误解”与三种应用层解决方案 Created: 2025年6月8日 07:20 Status: 完成\n在网络编程中，几乎每个开发者都听说过或遇到过“TCP粘包”问题。它听起来像一个网络协议的缺陷，但事实果真如此吗？\n本文将深入探讨“TCP粘包”现象的本质，解释为什么它其实是一个经典的“误解”，并详细介绍三种在应用层解决这个问题的经典方案。\n一、 核心误解：TCP 本身没有“包”的概念 在我们深入讨论之前，最重要的一点是：TCP 是一种面向字节流（Stream-Oriented）的协议，它本身没有“粘包”问题，因为它根本不认识“包”。\n我们可以把 TCP 连接想象成一根两端对等的水管。发送方（A端）往水管里倒水，可以一次倒一桶，也可以连续倒很多杯。对于接收方（B端）来说，它只能看到一股连续不断的水流从水管里流出，它并不知道A端是分几次、每次用多大的容器倒的水。\nTCP 的核心承诺是：\n可靠性：保证所有字节都会被对方收到。 有序性：保证字节的顺序与发送时的顺序一致。 但它不承诺保留发送方应用层写入操作的边界。换句话说，你在发送端调用了三次 send()，每次发送一个“消息包”，接收端完全可能通过一次 recv() 就接收到了这三个“消息包”的全部内容，或者只接收到第一个“消息包”的一部分。\n因此，所谓的“粘包”或“半包”问题，并非 TCP 的缺陷，而是应用层在处理无边界的字节流时遇到的挑战。\n二、 现象成因：为什么会“粘”在一起？ 既然是应用层的问题，那为什么会产生这种现象呢？主要有以下几个原因：\nTCP 发送缓冲区 (Send Buffer) 与 Nagle 算法：当应用层调用 send() 时，数据只是被拷贝到了操作系统的 TCP 发送缓冲区。为了提高网络效率，TCP 协议栈（特别是 Nagle 算法）可能会等待一小段时间，将多个小的发送请求合并成一个大的 TCP 段（Segment）再发送出去。 TCP 接收缓冲区 (Receive Buffer)：接收方收到的 TCP 段会存放在接收缓冲区。当应用层调用 recv() 时，它会从这个缓冲区里读取数据。如果此时缓冲区里已经到达了多个 TCP 段的数据，recv() 可能会一次性读取出来。 MSS/MTU 限制：如果应用层要发送的数据大于最大段大小（MSS），TCP 会自动将其拆分成多个 TCP 段。接收方应用层需要多次读取才能获得一个完整的应用层消息。 三、 问题的本质：如何在字节流中定义消息边界 既然 TCP 是无边界的，那么解决方案的核心就在于：发送方和接收方必须在应用层共同遵守一个协议，用来清晰地定义一条消息从哪里开始，到哪里结束。\n一旦接收方知道了消息的边界，它就可以从 TCP 字节流中准确地分割出一条条完整的消息。下面是三种最经典的实现方案。\n四、 三大经典应用层解决方案 1. 固定长度协议 (Fixed-Length Framing) 这是最简单直接的一种方法。\n原理：发送方和接收方约定，每一条应用层消息都具有固定的长度，例如 64 字节。 处理流程： 发送方：将消息封装成 64 字节。如果消息本身不足 64 字节，则用特殊字符（如空格、\\0）填充至 64 字节。 接收方：每次都从 TCP 流中读取 64 字节。一旦读满，就认为这是一个完整的消息，并将其交给上层业务逻辑处理。 优点：实现极其简单，没有复杂的解析逻辑。 缺点：灵活性极差，会造成带宽浪费（当消息远小于固定长度时），也无法处理大于固定长度的消息。 适用场景：适用于消息长度恒定不变的特定场景，在通用业务中很少使用。 2. 特殊分隔符协议 (Delimiter-based Framing) 这种方法通过一个特殊的标记来划分消息。\n原理：发送方和接收方约定一个不会在正常消息内容中出现的特殊字符或字符串序列（例如 \\r\\n 或自定义的结束符）作为消息的边界。 处理流程： 发送方：在每条消息的末尾添加这个特殊的分隔符。 接收方：不断从 TCP 流中读取数据并进行扫描，直到找到分隔符为止。从上一个分隔符到当前分隔符之间的数据，就是一条完整的消息。 真实案例：HTTP/1.1：一个绝佳的例子就是 HTTP 协议。它使用 \\r\\n 作为每行请求头/响应头的分隔符，并使用一个空的 \\r\\n\\r\\n 来标记整个头部的结束。 优点：实现相对简单，灵活性比固定长度协议高很多。 缺点： 转义问题：如果消息内容本身恰好包含了分隔符，就必须对内容中的分隔符进行转义，否则会导致消息被错误解析。这增加了处理的复杂性。 效率问题：接收方需要逐字节扫描数据以查找分隔符，当消息很大时可能会有性能开销。 3. 自定义消息结构：长度前缀协议 (Length-Prefixed Framing) 这是现代网络编程中最常用、最灵活、最可靠的方案。\n原理：在每条可变长度的消息数据（Body）前，附加一个固定长度的头部（Header）。这个头部中包含一个字段，明确地说明了紧随其后的 Body 部分有多长。 处理流程： 读取Header：接收方先从 TCP 流中读取固定长度的 Header（例如，先读 4 个字节）。 解析Body长度：接收方解析 Header，从中解码出表示 Body 长度的字段值，我们称之为 data_length。 读取Body：接收方根据上一步得到的 data_length，继续从 TCP 流中精确地读取 data_length 字节的数据。 组成完整消息：此时，“Header + Body” 就构成了一条完整的、无歧义的应用层消息。接收方可以开始处理这条消息，并重复步骤1来接收下一条消息。 优点： 边界清晰：通过长度前缀，可以精确地知道每条消息的边界，无需扫描内容。 高效灵活：可以传输任意长度的数据，没有数据浪费，解析效率高。 扩展性强：Header 中除了长度字段，还可以包含协议版本号、消息类型、压缩标志、序列号等丰富的元信息，非常便于未来对协议进行扩展。 缺点：实现上比固定长度协议稍复杂，需要处理好 Header 和 Body 的读取逻辑。 ","permalink":"https://asterzephyr.github.io/posts/tcp-/","summary":"\u003ch1 id=\"tcp粘包一个经典误解与三种应用层解决方案\"\u003eTCP粘包：一个经典“误解”与三种应用层解决方案\u003c/h1\u003e\n\u003cp\u003eCreated: 2025年6月8日 07:20\nStatus: 完成\u003c/p\u003e\n\u003cp\u003e在网络编程中，几乎每个开发者都听说过或遇到过“TCP粘包”问题。它听起来像一个网络协议的缺陷，但事实果真如此吗？\u003c/p\u003e\n\u003cp\u003e本文将深入探讨“TCP粘包”现象的本质，解释为什么它其实是一个经典的“误解”，并详细介绍三种在应用层解决这个问题的经典方案。\u003c/p\u003e","title":"TCP粘包：一个经典“误解”与三种应用层解决方案"},{"content":"工程估算与性能建模 第一部分：性能估算的常用计算模型 确实没有一个\u0026quot;万能公式\u0026quot;可以计算所有问题，但我们可以建立一些思维模型来进行估算。\n1. CPU 资源估算 核心思想: 总CPU时间 = 总请求数 × 平均单次请求处理耗时 估算公式: 单核CPU总处理时长 (秒) = QPS × 平均单次请求CPU耗时 (秒) 所需CPU核心数 = (单核CPU总处理时长 / 任务时间窗口秒数) / CPU目标使用率 解释: 平均单次请求CPU耗时: 这个数据需要通过**性能分析（Profiling）**来获取，这也是你之前做的\u0026quot;性能基准模型\u0026quot;的意义所在。 CPU目标使用率: 通常设为70%-80%。你不能假设CPU能100%跑满，必须留出余量应对突发流量和系统开销。 例子: QPS为2000，平均每个请求消耗CPU 10毫秒（0.01秒），希望CPU使用率不超过70%。 每秒需要的CPU总时间 = 2000 * 0.01 = 20秒 所需核心数 = 20 / 0.7 ≈ 28.57 -\u0026gt; 需要约 29个CPU核心。 2. I/O 资源估算 (网络 \u0026amp; 磁盘) 网络I/O: 所需网络带宽 (Mbps) = QPS × 平均请求/响应大小 (KB) × 8 / 1024 例子: QPS为2000，平均响应大小为50KB。 所需带宽 = 2000 * 50 * 8 / 1024 ≈ 781 Mbps 磁盘I/O: 所需IOPS (每秒读写次数) = 读取QPS + 写入QPS 所需磁盘吞吐 (MB/s) = (读取QPS × 平均读取大小) + (写入QPS × 平均写入大小) 关键点: 磁盘的瓶颈通常是 IOPS 和 延迟（latency），尤其是对于数据库这种需要大量随机读写的应用。 3. 内存资源估算 核心思想: 总内存 = 常驻内存 + (并发连接数 × 每个连接的内存) + 缓存 估算公式: 总内存占用 ≈ 基础服务内存 + (峰值并发数 × 单个请求平均内存开销) + 各类缓存大小 解释: 基础服务内存: 程序启动后，什么都不干时占用的内存。 单个请求平均内存开销: 处理一个请求时，创建的变量、对象、缓冲区等占用的内存。这个也需要通过压测和内存分析工具来获得。 缓存: 如Redis客户端缓存、本地缓存等，这部分通常是固定的。 第二部分：如何漂亮地回答GC问题？ 你当时的回答思路（从CPU指令去推算）体现了你的思考，但没有命中面试官想考察的核心点。我们来重构一下回答框架。\n面试官的问题: 1万个对象，每个2KB，做一次GC要多久？\nStep 1：反问与澄清（最关键的一步！） 一个资深工程师在面对模糊问题时，首先会去明确边界和上下文。这能瞬间体现你的专业性。\n\u0026ldquo;这个问题非常好，为了更准确地估算，我想先澄清几个前提条件：\u0026rdquo;\n\u0026ldquo;我们讨论的是哪种语言的GC？ 是Go，还是Java（用的G1、ZGC还是其他？），或者是Python？它们的GC策略和性能表现差异巨大。\u0026rdquo; \u0026ldquo;这1万个对象的内存结构是怎样的？ 它们是包含很多指针的复杂对象，还是扁平的结构体（struct）？扫描一个指针密集的堆，比扫描一个连续的内存块要慢得多。\u0026rdquo; \u0026ldquo;您问的\u0026rsquo;耗时\u0026rsquo;，是指GC导致的程序暂停（Stop-The-World, STW）时间，还是指GC在后台并发执行消耗的总CPU时间？\u0026rdquo; （这个问题是\u0026quot;王炸\u0026quot;，能直接体现你对现代GC的深刻理解）。 Step 2：基于假设进行建模估算（以Go语言为例） 假设面试官说：\u0026ldquo;就按你熟悉的Go语言，常规的指针对象，我关心的是STW暂停时间。\u0026rdquo;\n\u0026ldquo;好的，那我们基于Go的并发GC模型来分析：\u0026rdquo;\n计算总数据量: 总内存 = 10,000个对象 × 2 KB/对象 = 20,000 KB = 20 MB。 \u0026ldquo;首先，涉及的总内存是20MB，这是一个非常小的堆大小。\u0026rdquo; 拆解Go的GC工作: \u0026ldquo;Go的GC主要是并发执行的，它包含两个部分：一部分是极短的STW暂停，另一部分是与我们业务代码并行的标记和清扫工作。\u0026rdquo; 估算STW暂停时间 \u0026ldquo;对于现代的Go版本（如1.18+），其STW暂停时间已经优化得非常好，通常在亚毫秒级别（sub-millisecond），甚至几十微秒（microseconds）。更重要的是，Go的STW时间与堆大小基本无关，而主要与goroutine的数量和全局变量的扫描有关。所以，对于20MB的小堆，我们可以预期STW暂停时间非常短，可能在10到100微秒之间，对线上应用的影响微乎其微。\u0026rdquo; （补充一句）\u0026ldquo;当然，在非常老的Go版本（如1.5之前），STW可能会达到几毫秒甚至几十毫秒。\u0026rdquo; 估算并发执行耗时 至于并发部分，Go的GC默认会占用25%的CPU资源来执行标记和清扫。扫描20MB的内存对于现代CPU来说是非常快的，实际的CPU工作量可能也就在几毫秒。假设扫描20MB需要2毫秒的纯CPU时间，那么在25%的利用率下，它会在 2ms / 0.25 = 8ms 的时间跨度内完成。但这部分不会暂停我们的业务代码。\u0026quot; Step 3：总结与延伸 \u0026ldquo;综合来看，对于这个场景：\nSTW暂停时间: 10-100微秒，几乎可以忽略不计。 总GC耗时: 约8毫秒的时间跨度，但不影响业务逻辑执行。 这也解释了为什么Go在高并发、低延迟的场景下表现优异——它的GC设计优先保证了低延迟，而不是高吞吐。\u0026rdquo;\nStep 4：展示深度理解（加分项） \u0026ldquo;如果我们换个场景，比如Java的G1GC处理同样的数据：\nG1的目标是将STW控制在10毫秒以内，但实际可能在1-5毫秒。 如果是ZGC或Shenandoah，STW可能只有几百微秒，接近Go的水平。 但Java GC的吞吐量通常比Go更高，适合批处理场景。 这体现了不同GC算法的设计权衡：延迟 vs 吞吐量。\u0026rdquo;\n第三部分：性能建模的实践方法 1. 建立性能基准模型 微基准测试（Micro-benchmarks）: 测试单个函数或算法的性能 集成基准测试（Integration benchmarks）: 测试完整请求链路的性能 压力测试（Stress testing）: 测试系统在极限负载下的表现 2. 性能分析工具链 CPU Profiling: Go pprof, Java JProfiler, Linux perf 内存分析: Heap dumps, Memory profilers I/O分析: iostat, iotop, 应用层监控 3. 容量规划方法论 确定性能目标: SLA要求（延迟、吞吐量、可用性） 建立性能模型: 基于历史数据和基准测试 负载预测: 业务增长预期、流量模式分析 资源规划: 计算所需的CPU、内存、存储、网络资源 验证与调优: 通过压测验证规划的准确性 4. 性能优化的系统性方法 识别瓶颈: 通过监控和分析找到性能瓶颈 量化影响: 评估优化的潜在收益 实施优化: 代码优化、架构调整、资源扩容 验证效果: 通过A/B测试或灰度发布验证优化效果 结语 性能估算和建模是一门结合理论与实践的艺术。它需要我们：\n建立系统性的思维模型，而不是依赖经验和直觉 掌握量化分析的方法，用数据说话 理解系统的本质特征，抓住关键瓶颈 持续验证和迭代，不断完善模型的准确性 在面试中展现这种系统性思维，不仅能回答具体的技术问题，更能体现你作为工程师的专业素养和解决复杂问题的能力。\n","permalink":"https://asterzephyr.github.io/posts/engineering-estimation-performance-modeling/","summary":"\u003ch1 id=\"工程估算与性能建模\"\u003e工程估算与性能建模\u003c/h1\u003e\n\u003ch2 id=\"第一部分性能估算的常用计算模型\"\u003e第一部分：性能估算的常用计算模型\u003c/h2\u003e\n\u003cp\u003e确实没有一个\u0026quot;万能公式\u0026quot;可以计算所有问题，但我们可以建立一些思维模型来进行估算。\u003c/p\u003e\n\u003ch3 id=\"1-cpu-资源估算\"\u003e1. CPU 资源估算\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e核心思想\u003c/strong\u003e: 总CPU时间 = 总请求数 × 平均单次请求处理耗时\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e估算公式\u003c/strong\u003e:\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e单核CPU总处理时长 (秒) = QPS × 平均单次请求CPU耗时 (秒)\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e所需CPU核心数 = (单核CPU总处理时长 / 任务时间窗口秒数) / CPU目标使用率\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e解释\u003c/strong\u003e:\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e平均单次请求CPU耗时\u003c/strong\u003e: 这个数据需要通过**性能分析（Profiling）**来获取，这也是你之前做的\u0026quot;性能基准模型\u0026quot;的意义所在。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCPU目标使用率\u003c/strong\u003e: 通常设为70%-80%。你不能假设CPU能100%跑满，必须留出余量应对突发流量和系统开销。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e例子\u003c/strong\u003e: QPS为2000，平均每个请求消耗CPU 10毫秒（0.01秒），希望CPU使用率不超过70%。\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e每秒需要的CPU总时间 = 2000 * 0.01 = 20秒\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e所需核心数 = 20 / 0.7 ≈ 28.57\u003c/code\u003e -\u0026gt; 需要约 \u003cstrong\u003e29个CPU核心\u003c/strong\u003e。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"2-io-资源估算-网络--磁盘\"\u003e2. I/O 资源估算 (网络 \u0026amp; 磁盘)\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e网络I/O\u003c/strong\u003e:\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e所需网络带宽 (Mbps) = QPS × 平均请求/响应大小 (KB) × 8 / 1024\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e例子\u003c/strong\u003e: QPS为2000，平均响应大小为50KB。\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e所需带宽 = 2000 * 50 * 8 / 1024 ≈ 781 Mbps\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e磁盘I/O\u003c/strong\u003e:\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e所需IOPS (每秒读写次数) = 读取QPS + 写入QPS\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003ccode\u003e所需磁盘吞吐 (MB/s) = (读取QPS × 平均读取大小) + (写入QPS × 平均写入大小)\u003c/code\u003e\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e关键点\u003c/strong\u003e: 磁盘的瓶颈通常是 \u003cstrong\u003eIOPS\u003c/strong\u003e 和 \u003cstrong\u003e延迟（latency）\u003c/strong\u003e，尤其是对于数据库这种需要大量随机读写的应用。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"3-内存资源估算\"\u003e3. 内存资源估算\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e核心思想\u003c/strong\u003e: 总内存 = 常驻内存 + (并发连接数 × 每个连接的内存) + 缓存\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e估算公式\u003c/strong\u003e:\n\u003cul\u003e\n\u003cli\u003e\u003ccode\u003e总内存占用 ≈ 基础服务内存 + (峰值并发数 × 单个请求平均内存开销) + 各类缓存大小\u003c/code\u003e\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e解释\u003c/strong\u003e:\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e基础服务内存\u003c/strong\u003e: 程序启动后，什么都不干时占用的内存。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e单个请求平均内存开销\u003c/strong\u003e: 处理一个请求时，创建的变量、对象、缓冲区等占用的内存。这个也需要通过压测和内存分析工具来获得。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e缓存\u003c/strong\u003e: 如Redis客户端缓存、本地缓存等，这部分通常是固定的。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch2 id=\"第二部分如何漂亮地回答gc问题\"\u003e第二部分：如何漂亮地回答GC问题？\u003c/h2\u003e\n\u003cp\u003e你当时的回答思路（从CPU指令去推算）体现了你的思考，但没有命中面试官想考察的核心点。我们来重构一下回答框架。\u003c/p\u003e","title":"工程估算与性能建模"},{"content":"广告事件聚合系统设计笔记 Created: 2025年4月30日 11:39 Status: 完成\n1. 系统概述与目标 系统定义: 广告事件聚合系统是一个用于收集、处理和统计广告相关事件（如展示、点击）数据的系统。其核心目标是提供近乎实时的广告效果指标，并存储历史聚合数据以供分析。\n核心挑战: 该系统的主要挑战在于处理海量数据和高并发请求，同时保证数据的准确性和查询的低延迟。\n规模假设:\n广告数量: 50 million (5000万) 不同的 ad_id。 事件量: 点击事件 (Click Events): ~1 billion (10亿) / 天。 展示事件 (Impression Events): ~100 billion (1000亿) / 天 (基于约 1% 的 CTR - Click-Through Rate)。 总事件量: ~101 billion / 天 ≈ 100 billion / 天。 平均 QPS (Queries Per Second): 100B events / (24 * 3600s) ≈ 1.16 million QPS。 峰值 QPS: 假设为平均值的 3-5 倍，约为 3-5 million QPS。 2. 功能性需求 (Functional Requirements - FRs) 实时指标计算: 计算指定 ad_id 在过去一段时间内（例如，最近几分钟）的聚合指标，包括： 点击次数 (Click Count) 展示次数 (Impression Count) 点击率 (CTR = Clicks / Impressions) Top-K 广告: 展示最近一段时间内（例如，最近1分钟、10分钟、1小时）按特定指标（如点击量）排名的 Top-K 广告列表。 多维度聚合与过滤: 支持根据不同的维度或属性（例如，国家、用户设备、用户群体）对指标进行聚合和过滤查询。 历史数据存储: 将聚合后的分钟级指标数据持久化存储，并保留足够长的时间（例如，2年）。 3. 非功能性需求 (Non-Functional Requirements - NFRs) 可扩展性 (Scalability): 系统必须能够水平扩展以应对未来可能增长的数据量和流量。 高吞吐量 (High Throughput): 系统需要能够稳定处理峰值达到 3-5 million QPS 的事件写入。 低延迟 (Low Latency): 对于实时指标查询（FR1 \u0026amp; FR2），要求延迟小于 15 秒。这是一个比较严格的要求，尤其是在高吞吐量下。 数据准确性/完整性 (Data Correctness/Integrity): 由于广告数据直接关系到计费和预算，数据必须高度准确，不能出错或丢失。需要机制来确保最终一致性或进行对账。 容错性 (Fault Tolerance): 系统应能在部分组件或节点发生故障时继续运行，或能够快速恢复，保证数据不丢失。 高可用性 (High Availability): (隐含要求) 系统需要持续可用，尽量减少停机时间。 4. 数据量与带宽估算 单事件数据模型 (Event Data Model): ad_id: 广告ID timestamp: 事件发生时间戳 event_type: 事件类型 (\u0026lsquo;click\u0026rsquo;, \u0026lsquo;impression\u0026rsquo;, potentially others) user_id: 用户标识 (抽象表示) ip_address: 用户IP地址 dimensions: 其他维度信息 (e.g., country, device_type, demographics) 估计大小: 10 bytes \u0026lt; Event Size \u0026lt; 100 bytes。为计算方便，假设 0.1 KB / event。 网络带宽 (Ingestion Bandwidth): 平均: 1 million QPS * 0.1 KB/event ≈ 100 MB/s 峰值: 3 million QPS * 0.1 KB/event ≈ 300 MB/s (峰值按 3M QPS 计算) 结论: 网络带宽本身（几百MB/s）对于现代数据中心来说通常不是主要瓶颈。 原始数据存储 (Raw Data Storage): 日增量: 100 billion events/day * 0.1 KB/event ≈ 10 TB/day。 月增量 (30天): 10 TB/day * 30 days ≈ 300 TB/month。 考虑: 原始数据的存储时间可能受限于数据治理策略（如 GDPR 要求用户数据保留期限），30天是一个可能的参考值。 聚合数据存储 (Aggregated Data Storage): (将在存储层详细计算)5. 高层架构设计 (High-Level Architecture) 一个典型的流式处理系统架构可以分为以下几个主要层次：\n[图解标注 1: 高层架构图]\nData Collection: 负责接收来自前端或广告服务器的海量事件流。 Data Processing: 实时处理事件流，进行窗口聚合计算。 Data Storage: 存储处理后的聚合结果。 Query API: 提供接口供前端或其他服务查询聚合结果。 6. 详细组件设计与技术选型 6.1 数据收集层 (Data Collection / Ingestion) 核心挑战: 处理 3-5M QPS 的高并发写入，同时保证数据不丢失和系统稳定。\n技术选型分析:\n关系型数据库 (Relational DB - e.g., MySQL, PostgreSQL): 评估: 完全不可行。无法承受百万级 QPS 的写入压力。[排除] NoSQL 数据库 (e.g., Cassandra, HBase, Time-Series DB): 优点: 设计上支持高写入吞吐和水平扩展。 缺点: 集群规模: 假设单节点 15K W-OPS/sec，需要 3M / 15K = 200 个节点，集群规模庞大，运维复杂。\n热点问题 (Hotspot):\n写热点: 若以 ad_id 为分区键，热门广告会集中写入少数分区。缓解方法：加随机后缀 (ad_id_randomsuffix)，但增加读取复杂度。 读热点: 若后续处理需要按时间顺序读取（如最近5分钟数据），以 timestamp 作为 Sort Key 会导致最新数据集中在分区尾部，读取压力集中。缓解方法：更细粒度的分区（如按小时/分钟分区）、加 bucket。 复杂度: 需要复杂的 Sharding、Bucketing 策略和可能的定制优化，增加系统复杂度和运维成本。[可行性低，复杂度高]\n内存键值存储 (In-Memory KV Store - e.g., Redis): 优点: 极高的读写性能（单节点可达 100K+ OPS/sec），所需集群规模较小（~15-30个节点）。 缺点: 持久化: 基于内存，需配置持久化机制（如 AOF, RDB）。异步持久化有数据丢失风险（在两次持久化间隔内宕机）。 成本: 内存成本相对较高。 数据模型: 主要适合 KV，复杂查询能力弱。[备选，但持久化和成本是顾虑] 消息队列 / 流处理平台 (Message Queue / Streaming Platform - e.g., Kafka, Pulsar): [推荐选项] 优点: 高吞吐设计: 本身就是为高吞吐、持久化日志流设计的（Kafka 单 Broker 可处理 100K+ events/sec）。集群规模适中（~30 个 Broker）。 解耦与缓冲: 作为生产者和消费者之间的缓冲，削峰填谷，提高系统弹性。 持久化: 提供良好的数据持久化保证（磁盘存储，可配置副本）。 生态系统: 成熟的消费者 API，易于与下游流处理系统（Flink, Spark Streaming）集成。 缺点: 延迟: 相比直接写入 DB 或内存存储，增加了一层网络和处理延迟。 热点问题: 同样存在分区键选择问题。以 ad_id 为 Key 会导致热点。解决方案：ad_id + random_suffix 或其他组合键，确保分区负载均衡。 运维: 需要管理 Broker 集群（及 Zookeeper，如果使用 Kafka 旧版本）。 直接写日志文件 (Direct Log File Writing): 优点: 减少系统层级: 去掉消息队列层，可能降低端到端延迟。 简化运维: 文件系统通常比分布式消息队列更容易管理（表面上）。 缺点: IO 瓶颈: 可能将瓶颈转移到磁盘 I/O。需要优化写入（如 batch flush），但这会增加延迟。 文件管理: 需要处理文件滚动 (rotation)、合并、分发、跨节点协调等问题，复杂度不低。 消费复杂性: 下游系统消费文件不如消费 Kafka topic 方便。 压缩优势不明显: 存储相对廉价，压缩带来的成本节省可能不足以抵消复杂性增加。[可行性不高，潜在问题多] 结论: Kafka 是此场景下 Data Collection 层的优选方案，它在吞吐量、持久化、解耦和生态系统支持方面取得了较好的平衡。需要注意通过合理的分区策略（如 ad_id 加随机后缀）来避免热点问题。\n6.2 数据处理层 (Data Processing / Real-time Aggregation) 核心挑战: 在 \u0026lt; 15 秒的延迟要求下，对来自 Kafka 的 ~1M QPS (平均) 数据流进行聚合计算。\n技术选型分析:\n批处理 (Batch Processing - e.g., Hadoop MapReduce, Spark Batch): 评估: 延迟太高（小时级或天级），不满足 \u0026lt; 15 秒要求。[排除] 微批处理 (Mini-batch Processing - e.g., Spark Streaming): 评估: 延迟可以做到秒级。理论上可能满足 15 秒要求，但窗口处理和批次间隔需要精心调优，在高负载下可能延迟抖动较大。[备选，但流处理更优] 流处理 (Streaming Processing - e.g., Apache Flink, Kafka Streams, Storm): [推荐选项] 优点: 低延迟: 提供毫秒级到秒级的事件处理能力，最适合 \u0026lt; 15 秒的延迟要求。 事件驱动: 真正的按事件处理，状态管理更灵活。 窗口计算: 内建对时间窗口（滚动、滑动、会话）的强大支持，契合需求（计算过去 N 分钟指标）。 状态管理与容错: Flink 等框架提供强大的状态管理和 Checkpoint 机制，保证 Exactly-Once 或 At-Least-Once 语义。 缺点: 复杂度: 开发和运维门槛相对较高。 资源消耗: 状态管理和 Checkpoint 会带来额外的资源开销。 流处理关键问题与解决方案:\n处理速度 \u0026lt; 输入速度 (Backpressure):\n依赖上游 Kafka 作为缓冲。 流处理系统（如 Flink）应具备反压机制，通知上游减慢发送速度。 配置自动扩缩容 (Auto-scaling) 处理节点以匹配负载。 节点故障与恢复 (Fault Tolerance):\nCheckpointing: 定期将算子状态快照持久化到外部存储（如 HDFS, S3）。节点故障后，从最近的成功 Checkpoint 恢复状态并重新处理后续数据。 外部 Checkpoint 存储: 必须将 Checkpoint 存在独立于计算节点的可靠存储上。 Checkpoint 频率 vs. 延迟 vs. 恢复时间:\n高频 Checkpoint: 增加处理延迟和存储开销，但恢复快。 低频 Checkpoint: 减少正常处理开销，但恢复慢，需重算更多数据。 需要根据延迟要求和可接受的恢复时间进行权衡。 是否需要 Flink Checkpoint (如果 Kafka 已有 Offset 管理):\nKafka Offset: 记录了消费到哪个位置，保证了数据源的不丢失不重复（如果消费者幂等或事务性写入）。 Flink Checkpoint: 保存的是 计算状态 (如窗口内的部分聚合值)。 对于聚合计算: 必须使用 Flink Checkpoint (或类似机制)。如果 Flink Task 失败，仅从 Kafka Offset 恢复会丢失内存中的中间聚合状态，导致结果错误。 优化可能: 如果聚合窗口很短（如1分钟），且上游 Kafka 数据保留时间足够长，理论上 可以在 Task 失败后，从 Kafka 上一个窗口的起始 Offset 开始重新计算整个窗口的数据来恢复状态。这避免了 Flink 自身状态持久化的开销，但恢复时间会变长（需要重读并计算整个窗口的数据）。考虑到 15 秒延迟要求，频繁的小窗口计算+快速恢复可能更倾向于使用 Flink Checkpoint。 资源估算:\n假设 Flink 单 Task Manager (TM) 核心能处理 50K events/sec。 需要 3M QPS / 50K events/sec/core ≈ 60 个处理核心 (分布在多个 TM 上)。集群规模可接受。 热点问题 (Hotspot in Processing):\n如果上游 Kafka 通过加随机后缀打散热点，Flink 收到的数据应该是相对均匀的，处理层热点风险降低。 Flink 内部也可以进行 rebalance 或 keyBy 操作后的多并行度处理。 窗口策略:\n使用滑动窗口 (Sliding Window)，例如：窗口大小 1 分钟，滑动步长 10 秒。每 10 秒输出一次过去 1 分钟的聚合结果，满足 15 秒的刷新需求。 结论: Apache Flink 是此场景下 Data Processing 层的优选方案，其低延迟特性和强大的状态管理、窗口机制非常适合需求。需要仔细配置 Checkpoint 和资源。\n6.3 数据存储层 (Data Storage / Aggregated Data) 核心挑战: 存储长达 2 年的分钟级聚合数据，并支持对近期数据的快速查询 (\u0026lt; 15 秒)。\n数据模型 (Aggregated Table):\nCREATE TABLE ad_minute_metrics ( ad_id BIGINT, -- 广告ID timestamp_minute DATETIME, -- 聚合时间窗口（分钟精度） click_count BIGINT, -- 该分钟点击数 impression_count BIGINT, -- 该分钟展示数 -- Optional Dimensions (can be in separate dimension tables or flattened) country VARCHAR, device_type VARCHAR, -- ... other dimensions used for filtering/grouping PRIMARY KEY (ad_id, timestamp_minute, country, device_type, ...) -- Example composite key ); 存储量估算:\n行数/分钟: 最多 50 million ad_id (实际上远小于，只有活跃的广告才产生数据) 行数/天: 50M * 24 * 60 = 72 Billion (理论上限，非常夸张) 更现实的估计：假设峰值时段有 10% 的广告活跃，平均每天有 1% 的广告活跃。活跃广告每分钟产生一条聚合记录。 日增聚合记录数: (50M * 1%) * 24 * 60 ≈ 720 million rows/day (假设每个活跃广告每分钟都有数据) 单行聚合数据大小: 假设包括各种维度和指标，估计为 100 bytes (0.1 KB)。 (用户估算的 50KB 可能过大，除非包含非常多的维度信息或者原始事件样本) 日增存储 (聚合): 720M rows/day * 100 bytes/row ≈ 72 GB/day。 两年总存储 (聚合): 72 GB/day * 365 days/year * 2 years ≈ 52.5 TB。 (这个数量级是合理的，远小于原始数据量) 如果按用户之前估算的 3-5TB，意味着每天活跃的广告或聚合粒度更粗，或单行更小。我们暂按 50TB 级考虑，更具挑战性。 技术选型分析:\n关系型数据库: 评估: 存储 50TB+ 数据并进行快速聚合查询（尤其带过滤条件）性能会很差。[排除] NoSQL (KV Store, Document DB): 评估: 适合单点查询，但对于聚合、范围扫描、多维度过滤分析能力较弱。[不适合主要存储] OLAP (Online Analytical Processing) 数据库: [推荐选项] 例子: ClickHouse, Apache Doris, Apache Pinot, Druid。 优点: 列式存储: 高效压缩，查询时只读取所需列，非常适合聚合计算。 查询性能: 专门为分析查询优化，支持 SQL-like 接口。 可扩展性: 支持分布式部署和水平扩展。 缺点: 单点写入/更新性能通常不如 OLTP 或 NoSQL。但我们主要是批量写入聚合结果，可以接受。 OLAP 查询性能优化 (\u0026lt; 15 秒):\n数据分区 (Partitioning): 必须按时间分区 (e.g., 按天或按月分区)。查询近期数据时，只需扫描少量分区。 数据排序/索引 (Sorting/Indexing): 在分区内根据常用查询维度（如 timestamp_minute, ad_id）排序或建立索引（如 ClickHouse 的主键/跳数索引）。 冷热数据分离 (Hot/Cold Data Tiering): 热数据: 最近 7-30 天的数据（大约 0.5 - 2 TB）存储在高性能介质上（SSD）。 内存加速: 针对极热数据（如最近1天，约 72GB）或常用维度组合的查询结果，可以考虑放入内存（如 ClickHouse 的内存表或操作系统的 Page Cache）。100-200GB 内存对于现代服务器是可行的。 冷数据: 超过 30 天的数据存储在成本较低的 HDD 上。 预聚合/物化视图 (Pre-Aggregation / Materialized Views): 如果存在固定的、高频的查询模式（例如，按国家统计的总点击量），可以创建物化视图提前计算好结果。 缓存 (Caching): 在查询层（API Gateway 或应用层）增加缓存，缓存高频查询的结果。 利用 OLAP 数据库自身的查询缓存。 结论: 选择一个高性能的 OLAP 数据库 (如 ClickHouse 或 Doris) 作为聚合数据存储层，并结合时间分区、排序键/索引、冷热分离、可能的物化视图和缓存策略，来满足 2 年存储和 \u0026lt; 15 秒查询延迟的需求。\n6.4 查询接口层 (Query Interface / API) 提供一个 API 服务（例如，基于 RESTful 或 gRPC）。 该服务接收来自前端（仪表盘）或其他后端服务的查询请求。 将请求转换为底层 OLAP 数据库的 SQL (或特定 DSL) 查询。 执行查询并返回结果。 实现认证、授权、限流等标准 API 网关功能。 可以集成缓存逻辑。 7. 数据准确性与对账 (Data Correctness \u0026amp; Reconciliation) 问题背景: 流处理系统为了追求低延迟，可能面临事件乱序、事件迟到、处理错误、节点故障导致状态丢失（即使有 Checkpoint 也可能存在窗口边缘问题）等情况，导致实时结果与“真实”情况存在细微偏差。对于计费敏感的广告系统，需要机制来保证最终的数据准确性。\n解决方案: 引入对账 (Reconciliation) 机制，定期基于原始数据进行全量或增量计算，修正实时结果。\n两种主要架构模式:\nLambda 架构:\n结构: 同时运行两条处理链路： Speed Layer (速度层): 实时流处理（Kafka -\u0026gt; Flink -\u0026gt; OLAP），提供快速但可能不完全准确的结果。 Batch Layer (批处理层): 定期（如每天）运行批处理作业（如 Spark/MapReduce），读取一天内收集到的所有原始数据（可能存储在 HDFS 或对象存储中），进行精确计算。 Serving Layer (服务层): OLAP 数据库。批处理层的结果会 Upsert (Update or Insert) 到 OLAP 数据库中，覆盖或修正速度层写入的数据。 优点: 鲁棒性高，批处理层作为“黄金标准”保证最终准确性。技术成熟。 缺点: 复杂度高: 需要开发和维护两套逻辑相似但技术栈不同的代码（流处理逻辑 + 批处理逻辑）。 资源消耗大: 需要维护两套计算集群。 逻辑同步困难: 保持两套代码逻辑完全一致是个挑战。 [图解标注 2: Lambda 架构图] (展示 Speed Layer 和 Batch Layer 并行处理，最终写入 Serving Layer) Kappa 架构:\n结构: 只有一条流处理链路。对账通过数据回放 (Data Replay) 实现。 原始数据存储在具有长保留时间的消息队列（如 Kafka）或日志存储中。 当需要修正历史数据或进行对账时，从某个历史时间点开始，将原始数据重新注入（回放到）同一个流处理系统 (Flink) 中。 流处理系统以“重算模式”运行，计算出修正后的聚合结果，并 Upsert 到 OLAP 数据库中。 优点: 架构简化: 只需要维护一套代码和一套处理引擎。 资源效率: 避免了常驻的批处理集群。 缺点: 对流处理系统要求高: 需要流处理框架支持高效的数据回放、强大的状态管理和 Exactly-Once 语义保证。 回放可能影响实时处理: 需要隔离回放任务与实时任务的资源，或在低峰期进行。 无独立验证: 缺少了 Lambda 架构中独立的批处理层作为交叉验证。 [图解标注 3: Kappa 架构图] (展示单一流处理路径，并有从 Data Collection 回放数据到流处理引擎的循环路径) 选择考虑:\n如果团队对流处理技术（如 Flink）掌握深入，且框架能力足够强大，Kappa 架构因其简洁性更受青睐。 如果对数据准确性要求极高，且希望有独立的验证机制，或者流处理技术栈不够成熟，Lambda 架构可能是更稳妥的选择。 对于广告计费场景，通常对准确性要求极高，可能会倾向于 Lambda 或具备非常强一致性保证的 Kappa 实现。 8. 总结与权衡 广告事件聚合系统设计的核心在于平衡高吞吐 (3-5M QPS)、低延迟 (\u0026lt;15s) 和数据准确性这三个关键 NFR。 推荐技术栈: Kafka (收集) -\u0026gt; Flink (处理) -\u0026gt; ClickHouse/Doris (存储) 是一个常见的、能够满足需求的组合。 关键设计点: 在 Kafka 层使用分区键+随机后缀缓解热点。 在 Flink 层使用滑动窗口满足实时性，并配置外部 Checkpoint 保证容错。 在 OLAP 层使用时间分区、冷热分离、索引/排序优化查询性能。 引入Lambda 或 Kappa 架构进行数据对账，保证最终数据准确性。 重要权衡 (Trade-offs): 延迟 vs. 成本/复杂度: 选择流处理 (Flink) 获得了低延迟，但带来了更高的开发和运维复杂度。使用内存存储 (Redis) 可能延迟更低，但持久化和成本是问题。 简单性 vs. 准确性: 简单的流处理可能无法保证 100% 准确，引入 Lambda/Kappa 增加了系统复杂度以换取准确性。 Checkpoint 频率 vs. 性能/恢复速度: 需要根据实际需求调整。 ","permalink":"https://asterzephyr.github.io/posts/-/","summary":"\u003ch1 id=\"广告事件聚合系统设计笔记\"\u003e广告事件聚合系统设计笔记\u003c/h1\u003e\n\u003cp\u003eCreated: 2025年4月30日 11:39\nStatus: 完成\u003c/p\u003e\n\u003ch2 id=\"1-系统概述与目标\"\u003e1. 系统概述与目标\u003c/h2\u003e\n\u003cp\u003e\u003cstrong\u003e系统定义:\u003c/strong\u003e\n广告事件聚合系统是一个用于收集、处理和统计广告相关事件（如展示、点击）数据的系统。其核心目标是提供近乎实时的广告效果指标，并存储历史聚合数据以供分析。\u003c/p\u003e","title":"广告事件聚合系统设计笔记"},{"content":"如何写好prompt Created: 2025年6月1日 01:07 Status: 完成\nhttps://www.bilibili.com/video/BV1ZCjgzDEwA/?spm_id_from=333.1007.tianma.1-3-3.click\u0026amp;vd_source=836fe27208da24d6dc88c97a9c9c3b53\n开篇：Prompt 的重要性与生产环境的挑战\nPrompt 无处不在： 我们平时和大型模型对话，输入的每一句话其实都是一个 Prompt。 生产环境的严苛要求： 高精度： 与我们平时调试代码（可以三番五次试错）不同，线上应用的 Prompt 要求一次调用就能产生高精度的、可直接使用的结果。没有反复试错的机会，\u0026ldquo;调一次它就可能要出去，结果就要用了\u0026rdquo;。 高置信度： 目标是尽可能提升模型输出结果的置信度和准确度。 核心问题： 生产环境中的 Prompt 应该是什么样子？它应该由哪些方面组成，才能达到这种高要求？ Prompt 设计技巧概览\n单阶段技巧 (Single-Stage Techniques): 问题： 即使 Prompt 格式都满足了，模型输出的结果（“词性”）也可能不好。 目标： 分享业界通过 Prompt 提示词技巧来提升模型遵循指令 (follow 指令) 能力的实用方法，特别是那些“放上去之后发现效果很好”的技巧。 多阶段技巧 (Multi-Stage Techniques): 问题： 很多时候，模型很难在一个阶段、一个 Prompt 内完美执行复杂任务。 目标： 如何进一步提升执行准确度？讲者会分享实用的多阶段技巧。 举例：“反思”机制： 第一步：先生成一个结果。 第二步：模型进行自我反思（“诶？你看你给我这个结果对不对？”）。 第三步：基于反思再次生成。 关键： 哪些多阶段技巧是真正实用、精度高，并且适合放到线上跑的。 为什么 Prompt Engineering 如此关键？ (深层原因与架构)\n典型架构： 用户提问 -\u0026gt; 服务端 (Service) -\u0026gt; 调用大模型 API (传入 Prompt) -\u0026gt; 模型返回结果。 核心工作： 我们如何将用户的原始查询 (query) 通过精心设计的 Prompt，转化为大模型能够理解并产生高质量（高“信趣”）结果的输入。这正是 Prompt 技巧的用武之地。 LLM 并非万能神话： 破除迷信： 网上很多文章或宣传（Paper、公众号）把大模型吹得“无所不能”。 现实骨感： \u0026ldquo;真的当你去在工位去用的时候\u0026hellip;他们没有所谓的无所不能，他其实很多事情他做不到\u0026rdquo;。模型可能“什么东西他能够都能够通一点”，但真要上线作为业务去用，会发现“还是有很多很多的问题”。 Prompt 的作用： 正因为模型本身能力没那么强，所以需要通过 Prompt 技巧等多种方式（“打各种的补丁”）让它变得更准，使其准确率达到线上工业界可用的水平。 核心论证：不同 Prompt 带来的效果天壤之别 (岗位推荐案例详解)\n场景： 岗位推荐。\n简单 Prompt 示例:\n内容： \u0026ldquo;请帮我回答一下问题，下面的问题就是说客户真正给到你的这里面，比如说，嗯，客户当前的 query 为什么什么什么什么，然后岗位列表如下，请帮我找合适岗位，然后把岗位列表扔这\u0026hellip;\u0026rdquo; 模型输出： 可能直接给出一个岗位名称，例如“大模型研发算法”。 问题： 缺乏分析过程： “他是没有分析，你也不知道他为什么选这个”。 难以校验逻辑： 无法判断其选择的逻辑是否正确。 精心设计的详细 Prompt 示例 (融入了类似 COT - Chain of Thought 的思维链):\n第一步：赋予角色 (Persona): \u0026ldquo;假设你是一个知识渊博的 HR 专家，你拥有很强的这种岗位推荐能力。\u0026rdquo;\n第二步：明确任务与分析思路 (Task Requirements \u0026amp; Analysis Steps):\n“首先你就像你整个的分析思路，你首先要给我分析说这个用户 query 中表达的诉求，总结出用户新的诉求。” “然后分析岗位列表中每个岗位跟我这个诉求是否匹配，然后给出原因。” “最后输出合适的岗位，并按照匹配度进行这个排序。” 输入： 同样的 query 和岗位列表。\n模型输出 (预期)： 会先进行分析，然后给出带有理由的推荐和排序。\n为什么这种方式更好？—— COT 的核心思想解读：\nLLM 的本质： 大模型是基于“Next Word Prediction”（下一个词预测）的。 逻辑连贯性： “你要是每一下一个词跟上一个词都很相关的话，那最后推论都很准确。” 如果引导模型一步一步分析，使其每一步的输出都与上文紧密相关且符合逻辑，那么最终结果的准确性就会大大提高。 避免跳跃性思维： 如果直接让模型输出选择题的 ABCD，它没有一个思考过程，很可能出错（“ABCD 很可能前面都不是那么相关”）。 逐步推导的力量： “如果说你让他是一步一步去分析，那他每出错的下一个词都跟之前的怎么样都是非常相关的。那么最后顺理成章地输出 a 或者 b 或者 c 的时候，它准确率的概率就会更高。” 案例中的应用： 先分析用户到底要干嘛 (Query)。 再分析每个岗位跟用户诉求的关系。 模型内部完成这个“类似独白的”分析过程后，就能明白哪个最好，哪个最不好，最后顺序输出。 改进后的实际结果展示：\n详细分析： “用户希望寻找一个与大模型相关的推荐的算法岗位，强调了自己有推荐技术的背景这样\u0026hellip;这意味着那个用户期望的岗位涉及到大模型应用，尤其是在推荐系统的上下文中\u0026hellip;” 匹配度分析： 逐一分析岗位匹配度，例如“岗位一是怎样的较低，为什么？因为这个这样分析之后\u0026hellip;” 准确的排序列表： 输出的排序列表更准确，如第一个是“京东物流招聘大模型算法工程师”。这比简单 Prompt 输出的“大模型研发算法”更精准，因为它结合了用户的“推荐”背景和“大模型”需求，得到了有效分析。 案例小结： 同一个问题，不同的提示词（Pump/Palm），效果完全不同。这再次印证了大模型目前尚未达到“通俗能力”（什么都能解决得很好）的阶段。\nPrompt 的核心组成部分 (精讲“POM”/Prompt 的构成)\n讲者详细拆解了一个优秀的 Prompt 应该包含哪些方面：\n任务 (Task):\n定义： 明确、确切地告诉模型要执行什么事情。 重要性： “这个是一定要有的，什么都没有，这个是一定要有的”。这是 Prompt 的基石。 示例： “请帮我判断一下用户求职的 query 与给定的岗位是否匹配。” 示例 (Examples / Instances - 即 Few-shot learning):\n定义： 根据要执行的任务，给定一个或多个输入，并给出对应的期望输出。也可以没有示例 (Zero-shot)。 关键点： 质量而非数量： “这个示例其实放不是随便放的，你放一个特别简单、特别 easy 的那种，就是他一下就能说出来了，这就没有意义。” 示范性： 示例应该能够教会模型如何处理类似但更复杂的情况，而不是最简单的东西。 输出格式 (Output Format):\n定义： 对模型输出内容的格式要求，例如要求输出 JSON，或者自定义的结构。 核心目的： 方便下游提取： “核心是说你在后面提取的时候的一个方便，你怎么能够把这个内容结构化地提取出来？” 如果模型每次输出格式不一，就很难解析并给用户使用，或在后续流程中继续处理。 用户体验： 即使用户直接看，格式清晰的输出也比“乱七八糟的东西，一大段文字什么结构都没有”要好得多。 示例： 输出一个 JSON 格式，包含“分析”字段和“判定的结果是否匹配”字段。 角色 (Role):\n定义： 为模型设定一个身份、一个视角。 示例： “他是一个资深的 HR、商务专家”，“你是一个产品专家”，“你是一个算法专家”，“你是一个老板/学生/老师”。 重要性： “角色的定义是非常重要的啊”。 有助于模型站在该角色的角度去思考问题。 影响输出的语气、分析思路。 “网上有很多 paper 在研究说这个角色对于模型执行 follow up 的能力的一个影响”。虽然短交互可能不明显，但长交互或特定扮演场景下非常关键。 若无角色设定，模型可能以它默认的、可能是“机器人”的视角来回答，不一定符合你的要求。 任务要求 / 步骤 (Task Requirements / Analysis Steps):\n定义： 给定一个分析的思路，明确告诉模型“你要先做什么？再做什么？再做什么。” 与 COT 的关系： 网上常说的 COT (Chain of Thought) 的一句箴言是“请一步步分析”。但关键在于，“这个一步步，怎么个一步步？他怎么按照哪个一步步来？” 为什么要固定分析思路： 一致性： 避免模型每次处理相同任务时，内部的分析步骤不一样（“它这个一步步就会有点就是不一样”），导致结果不稳定。 可调试性 (Debug)： 如果分析思路固定，当模型出错时，更容易定位问题在哪一步，从而进行修正和优化（“就算他也出了问题，你也好 debug，你知道这里面应该加哪一步”）。 可控性： “你是能够控制，严格控制它的那个分析的这种东西的，按照你自己的设想”，所以说“你是大模型的神，就这个道理”。 融入业务经验： 这个分析思路本身就体现了你的业务经验，“你站在这个业务的角度觉得，诶，你觉得怎么做这个业务是好的？先干什么，再干什么，再干什么，它是好的”。 额外信息 (Additional Information / Knowledge Base):\n问题： 大模型总有它不知道的东西，因为它的训练数据是截止到某个时间的。 示例： 讲者提到如果模型训练时 Deepseek 还没出来，它就不知道 Deepseek 是什么。当被问及时，模型可能需要去搜索。 解决方案： 提供小型知识库/词典： 在 Prompt 中直接告诉模型这些它可能不知道的信息。例如：“CV 是，比方说 computer vision，对吧？然后大模型 LMM 表示大模型”。 形式： 可以通过补充形式（如配置文件）或通过检索（RAG - Retrieval Augmented Generation，讲者提到“我们现在那个 log 也是知识库的一种，它只是一个动态的需要每次就要去检索”）。 适用场景： 对于一些场景下特定、但模型可能不知道的词汇或概念，可以作为小词典补充。如果信息量很大，建议用检索方式。 内容格式 (Content Format - Prompt 本身的排版与强调):\n重要性： “你这个 prompt 要有什么样的格式放\u0026hellip;你给它一个很好的格式，它能够你非常理解这个东西的时候，他也能很好地执行”。就像人阅读排版混乱的文章会很困难一样。 常用方法： Markdown： # 表示一级标题，## 表示二级标题等。 划重点/强调 (Highlighting)： 重复： “重要的事情说三遍，你可以重复三遍。” 标记： 使用星号 等特殊符号来标记重要指令或信息（“你可以给他画上这种星号，告诉他这个是重点”）。 目的： 都是为了“去增加我们模型 follow 指令的能力的”。 ","permalink":"https://asterzephyr.github.io/posts/-prompt/","summary":"\u003ch1 id=\"如何写好prompt\"\u003e如何写好prompt\u003c/h1\u003e\n\u003cp\u003eCreated: 2025年6月1日 01:07\nStatus: 完成\u003c/p\u003e\n\u003cblockquote\u003e\n\u003cp\u003e\u003ca href=\"https://www.bilibili.com/video/BV1ZCjgzDEwA/?spm_id_from=333.1007.tianma.1-3-3.click\u0026amp;vd_source=836fe27208da24d6dc88c97a9c9c3b53\"\u003ehttps://www.bilibili.com/video/BV1ZCjgzDEwA/?spm_id_from=333.1007.tianma.1-3-3.click\u0026amp;vd_source=836fe27208da24d6dc88c97a9c9c3b53\u003c/a\u003e\u003c/p\u003e\u003c/blockquote\u003e\n\u003cp\u003e\u003cstrong\u003e开篇：Prompt 的重要性与生产环境的挑战\u003c/strong\u003e\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003ePrompt 无处不在：\u003c/strong\u003e 我们平时和大型模型对话，输入的每一句话其实都是一个 Prompt。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e生产环境的严苛要求：\u003c/strong\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e高精度：\u003c/strong\u003e 与我们平时调试代码（可以三番五次试错）不同，线上应用的 Prompt 要求一次调用就能产生高精度的、可直接使用的结果。没有反复试错的机会，\u0026ldquo;调一次它就可能要出去，结果就要用了\u0026rdquo;。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e高置信度：\u003c/strong\u003e 目标是尽可能提升模型输出结果的置信度和准确度。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e核心问题：\u003c/strong\u003e 生产环境中的 Prompt 应该是什么样子？它应该由哪些方面组成，才能达到这种高要求？\u003c/li\u003e\n\u003c/ul\u003e\n\u003cp\u003e\u003cstrong\u003ePrompt 设计技巧概览\u003c/strong\u003e\u003c/p\u003e","title":"如何写好prompt"},{"content":"高可用IM Created: 2025年1月19日 18:15 Status: 完成\n一套高可用实时消息系统实现 实时消息系统与消息队列系统 实时消息【即时通信】系统，有群聊和单聊两种方式，其形态异于消息队列：\n1 大量的 group 信息变动\n群聊形式的即时通信系统在正常服务形态下，瞬时可能有大量用户登入登出，而消息队列系统 producer/consumer group 内成员变动相对静止。\n2 近似有序\n即时通信系统【特别是群聊】的消息并发量远远大于消息队列系统，本人经历过的最厉害的即时通信系统，每秒消息通信量可达百万级，而消息队列系统每秒处理十万量级的消息就很了不起了。大并发消息量决定了用户可以容忍其消息的顺序的稍微异常【某家消息系统的乱序问题一直存在】。\n即时通信系统并非做不到消息严格有序，而是基于系统成本【人力、系统】考量，为了从 99.9% 级别的近似有序做到百分百的严格有序，其成本估计可增加十倍【所谓有损服务】。\n二者之间的本质差异，决定了即时通信系统的技术体系迥异于消息队列系统。\n1 极简实现 所谓群聊系统消息，就是一种群聊方式，譬如直播房间内的聊天对应的服务器端就是一个群聊消息系统。\n2017年9月初初步实现了一套极简的消息系统，其大致架构如下：\n系统名词解释：\n1 Client : 消息发布者【其位于服务端内，亦可叫做服务端群聊消息系统调用者，不要与APP Client混淆】，publisher；\n2 Proxy : 系统代理，对外统一接口，收集 Client 发来的消息转发给 Broker；\n3 Broker ：系统消息转发Server，Broker 会根据 Gateway Message 组织一个 RoomGatewayList【key为 RoomID，value为 Gateway IP:Port 地址列表】，然后把 Proxy 发来的消息转发到 Room 中所有成员登录的所有 Gateway；\n4 Router ：用户登录消息转发者，把 Gateway 转发来的用户登入登出消息转发给所有的 Broker；\n5 Gateway ：所有服务端的入口，接收合法客户端的连接，并把客户端的登录登出消息通过 Router 转发给所有的 Broker；\n6 Room Message : Room 聊天消息；\n7 Gateway Message : Room 内某成员 登录 或者 登出 某Gateway消息，包含用户 UIN/RoomID/Gateway 地址 {IP:Port} 等消息；\n当一个 Room 中多个用户的 APP 连接一个 Gateway 的时候，Broker只会根据 RoomID 把房间内的消息转发一次给这个Gateway，由Gateway再把消息复制多份分别发送给连接这个 Gateway 的 Room 中的所有用户的客户端。\n这套系统有如下特点：\n1 系统只转发房间内的聊天消息，每个节点收到后立即转发出去，不存储任何房间内的聊天消息，不考虑消息丢失以及消息重复的问题； 2 系统固定地由一个 Proxy、三个 Broker 和一个 Router 构成； 3 Proxy 接收后端发送来的房间消息，然后按照一定的负载均衡算法把消息发往某个 Broker，Broker 则把消息发送到所有与 Room 有关系的接口机 Gateway； 4 Router 接收 Gateway 转发来的某个 Room 内某成员在这个 Gateway 的登出或者登录消息，然后把消息发送到所有 Broker； 5 Broker 收到 Router 转发来的 Gateway 消息后，更新（添加或者删除）与某 Room 相关的 Gateway 集合记录； 6 整个系统的通信链路采用 UDP 通信方式； 从以上特点，整个消息系统足够简单，不考虑扩缩容问题，当系统负载到达极限的时候，就重新再部署一套系统以应对后端client的消息压力。\n这种处理方式本质是把系统的扩容能力甩锅给了后端 Client 以及前端 Gateway：每次扩容一个系统，所有 Client 需要在本地配置文件中添加一个 Proxy 地址然后全部重启，所有 Gateway 则需要再本地配置文件添加一个 Router 地址然后全部重启。\n这种“幸福我一人，辛苦千万家”的扩容应对方式，必然导致公司内部这套系统的使用者怨声载道，升级之路就是必然的了。\n2 可扩展 大道之行也，天下为公，不同的系统有不同的构架，相同的系统总有类似的实现。类似于数据库的分库分表【关于分库分表，目前看到的最好的文章是参考文档1】，其扩展实现核心思想是分 Partition 分 Replica，但各 Replica 之间还区分leader（leader-follower，只有leader可接受写请求）和 non-leader（所有replica均可接收写请求）两种机制。\n从数据角度来看，这套系统接收两种消息：Room Message（房间聊天消息）和 Gateway Message（用户登录消息）。两种消息的交汇之地就是Broker，所以应对扩展的紧要地方就是 Broker，Broker 的每个 Partition 采用 non-leader 机制，各 replica 均可接收 Gateway Message 消息写请求和 Room Message 转发请求。\n首先，当 Room Message 量加大时可以对 Proxy 进行水平扩展，多部署 Proxy 即可因应 Room Message 的流量。\n其次，当 Gateway Message 量加大时可以对 Router 进行水平扩展，多部署 Router 即可因应 Gateway Message 的流量。\n最后，两种消息的交汇之地 Broker 如何扩展呢？可以把若干 Broker Replica 组成一个 Partition，因为 Gateway Message 是在一个 Partition 内广播的，所有 Broker Replica 都会有相同的 RoomGatewayList 数据，因此当 Gateway Message 增加时扩容 Partition 即可。当 Room Message 量增加时，水平扩容Partition 内的 Broker Replica 即可，因为 Room Message 只会发送到 Partition 内某个R eplica 上。\n从个人经验来看，Room ID 的增长以及 Room 内成员的增加量在一段时间内可以认为是线性增加，而 Room Message 可能会以指数级增长，所以若部署得当则 Partition 扩容的概率很小，而 Partition 内 Replica 水平增长的概率几乎是 100%。\n不管是 Partition 级别的水平扩容还是 Partition Replica 级别的水平扩容，不可能像系统极简版本那样每次扩容后都需要 Client 或者 Gateway 去更新配置文件然后重启，因应之道就是可用 zookeeper 充当角色的 Registry。通过这个 zookeeper 注册中心，相关角色扩容的时候在 Registry 注册后，与之相关的其他模块得到通知即可获取其地址等信息。采用 zookeeper 作为 Registry 的时候，所以程序实现的时候采用实时watch和定时轮询的策略保证数据可靠性，因为一旦网络有任何的抖动，zk就会认为客户端已经宕机把链接关闭。\n分析完毕，与之相对的架构图如下：\n分章节描述各个模块详细流程。\n2.1 Client Client详细流程如下：\n1 从配置文件加载 Registry 地址； 2 从 Registy 路径 /pubsub/proxy(Proxy 注册路径) 下获取所有的 Proxy，依据各个 Proxy ID 大小顺序递增组成一个 ProxyArray； 3 启动一个线程实时关注 Registry 路径 /pubsub/proxy，以获取Proxy的动态变化，及时更新 ProxyArray； 4 启动一个线程定时轮询获取 Registry 路径 /pubsub/proxy 下各个 Proxy 实例，作为关注策略的补充，以期本地ProxyArray 内各个 Proxy 成员与 Registry 上的各个 Proxy 保持一致；定时给各个 Proxy 发送心跳，异步获取心跳回包；定时清除 ProxyArray 中心跳超时的 Proxy 成员； 5 发送消息的时候采用 snowflake 算法给每个消息分配一个 MessageID ，然后采用相关负载均衡算法把消息转发给某个 Proxy。 本系统使用 Zookeeper 作为 Registry，而 Zookeeper 的通知机制有名的不可靠且延迟高（15s），所以采用 Zookeeper 自身的 Watch 机制的同时使用定时查询策略作为补偿机制。定时策略的定时时间单位应该是分钟级，不能小于 Zookeeper 自身的通知延时时长，以防止 Zookeeper 系统的轻微抖动造成消息系统的服务波动。\n2.2 Proxy Proxy详细流程如下：\n1 读取配置文件，获取 Registry 地址；\n2 把自身信息注册到 Registry 路径 /pubsub/proxy 下，把 Registry 返回的 ID 作为自身 ReplicaID；\n3 从 Registry 路径 /pubsub/broker/partition(x) 下获取每个 Broker Partition 的各个 replica；\n4 从 Registry 路径 /pubsub/broker/partition_num 获取当前有效的 Broker Partition Number；\n5 启动一个线程关注 Registry 上的 Broker 路径 /pubsub/broker，以实时获取以下信息：\nBroker Partition Number； ​新的Broker Partition（此时发生了扩容）； Broker Partition内新的broker replica（Partition内发生了replica扩容）； Broker Parition内某replica挂掉的信息；\n6 定时向各个 Broker replica 发送心跳，异步等待 Broker 返回的心跳响应包，以探测其活性，以保证不向超时的 replica 转发 Room Message；\n7 启动一个线程定时读取 Registry 上的 Broker 路径 /pubsub/broker 下各个子节点的值，以定时轮询的策略观察 Broker Partition Number 变动，以及各 Partition 的变动情况，作为实时策略的补充；同时定时检查心跳包超时的 Broker，从有效的 BrokerList 中删除；\n8 依据规则BrokerPartitionID = RoomID % BrokerPartitionNum， BrokerReplicaID = RoomID % BrokerPartitionReplicaNum 向某个 Partition 的 replica 转发 Room Message，收到 Client 的 Heatbeat 包时要及时给予响应。\n之所以把 Room Message 和 Heartbeat Message 放在一个线程处理，是为了防止进程假死这种情况。\n当 /pubsub/broker/partition_num 的值发生改变的时候(譬如值改为4)，意味着 Router Partition 进行了扩展，Proxy 需及时获取新 Partition 路径（如 /pubsub/broker/Partition2 和 /pubsub/broker/Partition3）下的实例，并关注这些路径，获取新 Partition 下的实例。\n之所以 Proxy 在获取 Registry 下所有当前的 Broker 实例信息后再注册自身信息，是因为此时 Broker 子系统扩容完成，才具有转发消息的资格。\nProxy 转发某个 Room 消息时候，只发送给处于 Running 状态的 Broker。为 Broker Partition 内所有 replica 依据Registry 给其分配的 replicaID 进行递增排序，组成一个 Broker Replica Array，规则中BrokerPartitionReplicaNum 为 Array 的 size，而 BrokerReplicaID 为 replica 在 Array 中的下标。\n2.2.1 Pipeline 收到的 Room Message 需要做三部工作：收取 Room Message、消息协议转换和向 Broker 发送消息。\n初始系统这三步流程如果均放在一个线程内处理，proxy 的整体吞吐率只有 50 000 Msg/s，最后的实现方式是按照消息处理的三个步骤以 pipeline 方式做如下流程处理：\n1 启动 1 个消息接收线程和 N【N == Broker Parition 数目】个多写一读形式的无锁队列【称之为消息协议转换队列】，消息接收线程分别启动一个 epoll 循环流程收取消息，然后把消息以相应的 hash 算法【队列ID = UIN % N】写入对应的消息协议转换队列； 2 启动 N 个线程 和 N * 3 个一写一读的无锁队列【称之为消息发送队列】，每个消息协议专家线程从消息协议转换队列接收到消息并进行协议转换后，根据相应的 hash 算法【队列ID = UIN % 3N】写入消息发送队列； 3 启动 3N 个消息发送线程，分别创建与之对应的 Broker 的连接，每个线程单独从对应的某个消息发送队列接收消息然后发送出去。 经过以上流水线改造后，Proxy 的整体吞吐率可达 200 000 Msg/s。\n2.2.2 大房间消息处理 每个 Room 的人数不均，最简便的解决方法就是给不同人数量级的 Room 各搭建一套消息系统，不用修改任何代码。\n然所谓需求推动架构改进，在系统迭代升级过程中遇到了这样一个需求：业务方有一个全国 Room，用于给所有在线用户进行消息推送。针对这个需求，不可能为了一个这样的 Room 单独搭建一套系统，况且这个 Room 的消息量很少。\n如果把这个 Room 的消息直接发送给现有系统，它有可能影响其他 Room 的消息发送：消息系统是一个写放大的系统，全国 Room 内有系统所有的在线用户，每次发送都会卡顿其他 Room 的消息发送。\n最终的解决方案是：使用类似于分区的方法，把这样的大 Room 映射为 64 个虚拟 Room【称之为 VRoom】。在 Room 号段分配业务线的配合下，给消息系统专门保留了一个号段，用于这种大 Room 的切分，在 Proxy 层依据一个 hash 方法 【 VRoomID = UserID % 64】 把每个 User 分配到相应的 VRoom，其他模块代码不用修改即完成了大 Room 消息的路由。\n2.3 Broker Broker详细流程如下：\n1 Broker 加载配置，获取自身所在 Partition 的 ID（假设为3）； 2 向 Registry 路径 /pubsub/broker/partition3 注册，设置其状态为 Init ，注册中心返回的 ID 作为自身的 ID(replicaID)； 3 接收 Router 转发来的 Gateway Message，放入 GatewayMessageQueue； 4 从 Database 加载数据，把自身所在的 Broker Partition 所应该负责的 RoomGatewayList 数据加载进来； 5 异步处理 GatewayMessageQueue 内的 Gateway Message，只处理满足规则【PartitionID == RoomID % PartitionNum】的消息，把数据存入本地路由信息缓存； 6 修改 Registry 路径 /pubsub/broker/partition3 下自身节点的状态为Running； 7 启动线程实时关注 Registry 路径 /pubsub/broker/partition_num 的值； 8 启动线程定时查询 Registry 路径 /pubsub/broker/partition_num 的值； 9 当 Registry 路径 /pubsub/broker/partition_num 的值发生改变的时候，依据规则【PartitionID == RoomID % PartitionNum】清洗本地路由信息缓存中每条数据； 10 接收 Proxy 发来的 Room Message，依据 RoomID 从路由信息缓存中查找 Room 有成员登陆的所有 Gateway，把消息转发给这些 Gateway； 注意 Broker 之所以先注册然后再加载 Database 中的数据，是为了在加载数据的时候同时接收 Router 转发来的 Gateway Message，但是在数据加载完前这些受到的数据先被缓存起来，待所有 RoomGatewayList 数据加载完后就把这些数据重放一遍；\nBroker 之所以区分状态，是为了在加载完毕 RoomGatewayList 数据前不对 Proxy 提供转发消息的服务，同时也方便 Broker Partition 应对消息量增大时进行水平扩展。\n当 Broker 发生 Partition 扩展的时候，新的 Partition 个数必须是 2 的幂，只有新 Partition 内所有 Broker Replica 都加载实例完毕，再更改 /pubsub/broker/partition_num 的值。\n老的 Broker 也要 watch 路径 /pubsub/broker/partition_num 的值，当这个值增加的时候，它也需要清洗本地的路由信息缓存。\nBroker 的扩容过程犹如细胞分裂，形成中的两个细胞有着完全相同的数据，分裂完成后【Registry路径 /pubsub/broker/partition_num 的值翻倍】则需要清洗垃圾信息。这种方法称为翻倍法。\n2.4 Router Router详细流程如下：\n1 Router 加载配置文件中的 Registry 地址；\n2 把自身信息注册到 Registry 路径 /pubsub/router 下，把 Registry 返回的 ID 作为自身 ReplicaID；\n3 从 Registry 路径 /pubsub/broker/partition(x) 下获取每个 Broker Partition 的各个 replica；\n4 从 Registry 路径 /pubsub/broker/partition_num 获取当前有效的 Broker Partition Number；\n5 启动一个线程关注 Registry 上的 Broker 路径 /pubsub/broker，以实时获取以下信息：\nBroker Partition Number； ​新的Broker Partition（此时发生了扩容）； Broker Partition内新的broker replica（Partition内发生了replica扩容）； Broker Parition内某replica挂掉的信息；\n6 定时向各个 Broker replica 发送心跳，异步等待 Broker 返回的心跳响应包，以探测其活性，以保证不向超时的 replica 转发 Gateway Message；\n7 启动一个线程定时读取 Registry 上的 Broker 路径 /pubsub/broker 下各个子节点的值，以定时轮询的策略观察 Broker Partition Number 变动，以及各 Partition 的变动情况，作为实时策略的补充；同时定时检查心跳包超时的 Broker，从有效的 BrokerList 中删除；\n8 从 Database 全量加载路由 RoomGatewayList 数据放入本地缓存；\n9 收取 Gateway 发来的客户端心跳消息，及时返回 ack 包；\n10 收取 Gateway 转发来的 Gateway Message，按照一定规则【BrokerPartitionID % BrokerPartitionNum = RoomID % BrokerPartitionNum】转发给，保证 Partition 下所有 replica 拥有同样的路由 RoomGatewayList 数据，再把 Message 内数据存入本地缓存，当检测到数据不重复的时候把数据异步写入 Database；\n*某个 Broker Partition 下所有 Broker Replica ** 2.5 Gateway Gateway详细流程如下：\n1 读取配置文件，加载 Registry 地址； 2 获取 Registry 路径 /pubsub/router/ 下所有 router replica，依据各 Replica 的 ID 递增排序组成 replica 数组 RouterArray； 3 启动一个线程实时关注 Registry 路径 /pubsub/router，以获取 Router 的动态变化，及时更新 RouterArray； 4 启动一个线程定时轮询获取 Registry 路径 /pubsub/router 下各个Router实例，作为关注策略的补充，以期本地 RouterArray 及时更新；定时给各个 Router 发送心跳，异步获取心跳回包；定时清除 RouterArray 中心跳超时的 Router 成员； 5 当有 Room 内某成员客户端连接上来或者 Room 内所有成员都不连接当前 Gateway 节点时，依据规则【RouterArrayIndex = RoomID % RouterNum】向某个 Router 发送 Gateway Message； 6 收到 Broker 转发来的 Room Message 时，根据 MessageID 进行去重，如果不重复则把消息发送到连接到当前 Gateway 的 Room 内所有客户端，同时把 MessageID 缓存起来以用于去重判断。 Gateway 本地有一个基于共享内存的 LRU Cache，存储最近一段时间发送的消息的 MessageID。\n3 系统稳定性 系统具有了可扩展性仅仅是系统可用的初步，整个系统要保证最低粒度的 SLA（0.99），就必须在两个维度对系统的可靠性就行感知：消息延迟和系统内部组件的高可用。\n3.1 消息延迟 准确的消息延迟的统计，通用的做法可以基于日志系统对系统所有消息或者以一定概率抽样后进行统计，但限于人力目前没有这样做。\n目前使用了一个方法：通过一种构造一组伪用户 ID，定时地把消息发送给 proxy，每条消息经过一层就把在这层的进入时间和发出时间以及组件自身的一些信息填入消息，这组伪用户的消息最终会被发送到一个伪 Gateway 端，伪 Gateway 对这些消息的信息进行归并统计后，即可计算出当前系统的平均消息延迟时间。\n通过所有消息的平均延迟可以评估系统的整体性能。同时，因为系统消息路由的哈希方式已知，当固定时间内伪 Gateway 没有收到消息时，就把消息当做发送失败，当某条链路失败一定次数后就可以产生告警了。\n3.2 高可用 上面的方法同时能够检测某个链路是否出问题，但是链路具体出问题的点无法判断，且实时性无法保证。\n为了保证各个组件的高可用，系统引入了另一种评估方法：每个层次都给后端组件发送心跳包，通过心跳包的延迟和成功率判断其下一级组件的当前的可用状态。\n譬如 proxy 定时给每个 Partition 内每个 broker 发送心跳，可以依据心跳的成功率来快速判断 broker 是否处于“假死”状态（最近业务就遇到过 broker 进程还活着，但是对任何收到的消息都不处理的情况）。\n同时依靠心跳包的延迟还可以判断 broker 的处理能力，基于此延迟值可在同一 Partition 内多 broker 端进行负载均衡。\n4 消息可靠性 公司内部内部原有一个走 tcp 通道的群聊消息系统，但是经过元旦一次大事故（几乎全线崩溃）后，相关业务的一些重要消息改走这套基于 UDP 的群聊消息系统了。这些消息如服务端下达给客户端的游戏动作指令，是不允许丢失的，但其特点是相对于聊天消息来说量非常小（单人1秒最多一个），所以需要在目前UDP链路传递消息的基础之上再构建一个可靠消息链路。\n国内某 IM 大厂的消息系统也是以 UDP 链路为基础的，他们的做法是消息重试加 ack 构建了可靠消息稳定传输链路。但是这种做法会降低系统的吞吐率，所以需要独辟蹊径。\nUDP 通信的本质就是伪装的 IP 通信，TCP 自身的稳定性无非是重传、去重和 ack，所以不考虑消息顺序性的情况下可以通过重传与去重来保证消息的可靠性。\n基于目前系统的可靠消息传输流程如下：\n1 Client 给每个命令消息依据 snowflake 算法配置一个 ID，复制三份，立即发送给不同的 Proxy； 2 Proxy 收到命令消息以后随机发送给一个 Broker； 3 Broker 收到后传输给 Gateway； 4 Gateway 接收到命令消息后根据消息 ID 进行重复判断，如果重复则丢弃，否则就发送给 APP，并缓存之。 正常的消息在群聊消息系统中传输时，Proxy 会根据消息的 Room ID 传递给固定的 Broker，以保证消息的有序性。\n5 Router群集 当线上需要部署多套群聊消息系统的时候，Gateway 需要把同样的 Room Message 复制多份转发给多套群聊消息系统，会增大 Gateway 压力，可以把 Router 单独独立部署，然后把 Room Message 向所有的群聊消息系统转发。\nRouter 系统原有流程是：Gateway 按照 Room ID 把消息转发给某个 Router，然后 Router 把消息转发给下游 Broker 实例。新部署一套群聊消息系统的时候，新系统 Broker 的schema 需要通过一套约定机制通知 Router，使得 Router 自身逻辑过于复杂。\n重构后的 Router 架构参照上图，也采用分 Partition 分 Replica 设计，Partition 内部各 Replica 之间采用 non-leader 机制；各 Router Replica 不会主动把Gateway Message 内容 push 给各 Broker，而是各 Broker 主动通过心跳包形式向 Router Partition 内某个 Replica 注册，而后此 Replica 才会把消息转发到这个 Broker 上。\n类似于 Broker，Router Partition 也以 2 倍扩容方式进行 Partition 水平扩展，并通过一定机制保证扩容或者 Partition 内部各个实例停止运行或者新启动时，尽力保证数据的一致性。\nRouter Replica 收到 Gateway Message 后，replica 先把 Gateway Message 转发给 Partition 内各个 peer replica，然后再转发给各个订阅者。Router 转发消息的同时异步把消息数据写入 Database。\n独立 Router 架构下，下面分别详述 Gateway、Router 和 Broker 三个相关模块的详细流程。\n5.1 Gateway Gateway详细流程如下：\n1 从 Registry 路径 /pubsub/router/partition(x) 下获取每个 Partition 的各个 replica；\n2 从 Registry 路径 /pubsub/router/partition_num 获取当前有效的 Router Partition Number；\n3 启动一个线程关注 Registry 上的 Router 路径 /pubsub/router，以实时获取以下信息：\nRouter Partition Number； 新的Router Partition（此时发生了扩容）； Partition内新的replica（Partition内发生了replica扩容）； Parition内某replica挂掉的信息；\n4 定时向各个 Partition replica 发送心跳，异步等待 Router 返回的心跳响应包，以探测其活性，以保证不向超时的 replica 转发 Gateway Message；\n4 启动一个线程定时读取 Registry 的 Router 路径 /pubsub/router 下各个子节点的值，以定时轮询的策略观察 Router Partition Number 变动，以及各 Partition 的变动情况，作为实时策略的补充；同时定时检查心跳包超时的 Router，从有效的 BrokerList 中删除；\n6 依据规则向某个 Partition 的 replica 转发 Gateway Message；\n第六步的规则决定了 Gateway Message 的目的 Partition 和 replica，规则内容有：\n如果某 Router Partition ID 满足 condition RoomID % RouterPartitionNumber == RouterPartitionID % RouterPartitionNumber，则把消息转发到此 Partition；\n这里之所以不采用直接 hash 方式 RouterPartitionID = RoomID % RouterPartitionNumber 获取 Router Partition，是考虑到当 Router 进行 2 倍扩容的时候当所有新的 Partition 的所有 Replica 都启动完毕且数据一致时才会修改 Registry 路径 /pubsub/router/partition_num 的值，按照规则的计算公式才能保证新 Partition 的各个 Replica 在启动过程中就可以得到 Gateway Message，也即此时每个 Gateway Message 会被发送到两个 Router Partition。当 Router 扩容完毕，修改 Registry 路径 /pubsub/router/partition_num 的值后，此时新集群进入稳定期，每个 Gateway Message 只会被发送固定的一个Partition，condition RoomID % RouterPartitionNumber == RouterPartitionID % RouterPartitionNumber 等效于 RouterPartitionID = RoomID % RouterPartitionNumber。\n如果 Router Partition 内某 replia 满足 condition replicaPartitionID = RoomID % ReplicaNumber，则把消息转发到此replica。\nreplica 向 Registry 注册的时候得到的 ID 称之为 replicaID，Router Parition 内所有 replica 按照 replicaID 递增排序组成 replica 数组 RouterPartitionReplicaArray，replicaPartitionID 即为 replica 在数组中的下标。\n5.1.1 Gateway Message 数据一致性 Gateway 向 Router 发送的 Router Message 内容有两种：某 user 在当前 Gateway 上进入某 Room 和某 user 在当前 Gateway 上退出某 Room，数据项分别是 UIN（用户ID）、Room ID、Gateway Addr和User Action(Login or Logout)。\n由于所有消息都是走 UDP 链路进行转发，则这些消息的顺序就有可能乱序。Gateway 可以统一给其发出的所有消息分配一个全局递增的 ID【下文称为 GatewayMsgID，Gateway Message ID】以保证消息的唯一性和全局有序性。\nGateway 向 Registry 注册临时有序节点时，Registry 会给 Gateway 分配一个 ID，Gateway 可以用这个 ID 作为自身的 Instance ID【假设这个ID上限是 65535】。\nGatewayMsgID 字长是 64 bit，其格式如下：\n// 63 -------------------------- 48 47 -------------- 38 37 ------------ 0 // | 16bit Gateway Instance ID | 10bit Reserve | 38bit自增码 |\n5.2 Router Router 系统部署之前，先设置 Registry 路径 /pubsub/router/partition_num 的值为1。\nRouter 详细流程如下：\n1 Router 加载配置，获取自身所在 Partition 的 ID（假设为3）；\n2 向 Registry 路径 /pubsub/router/partition3 注册，设置其状态为 Init，注册中心返回的 ID 作为自身的 ID(replicaID)；\n3 注册完毕会收到 Gateway 发来的 Gateway Message 以及 Broker 发来的心跳消息（HeartBeat Message），先缓存到消息队列 MessageQueue；\n4 从 Registry 路径 /pubsub/router/partition3 下获取自身所在的 Partition 内的各个 replica；\n5 从 Registry 路径 /pubsub/router/partition_num 获取当前有效的 Router Partition Number；\n6 启动一个线程关注 Registry 路径 /pubsub/router，以实时获取以下信息：\nRouter Partition Number； Partition内新的replica（Partition内发生了replica扩容）； Parition内某replica挂掉的信息；\n7 从 Database 加载数据；\n8 启动一个线程异步处理 MessageQueue 内的 Gateway Message，把 Gateway Message 转发给同 Partition 内其他 peer replica，然后依据规则【RoomID % BrokerPartitionNumber == BrokerReplicaPartitionID % BrokerPartitionNumber】转发给 BrokerList 内每个 Broker；处理 Broker 发来的心跳包，把 Broker 的信息存入本地 BrokerList，然后给 Broker 发送回包；\n9 修改 Registry 路径 /pubsub/router/partition3 下节点的状态为 Running；\n10 启动一个线程定时读取 Registry 路径 /pubsub/router 下各个子路径的值，以定时轮询的策略观察 Router 各 Partition 的变动情况，作为实时策略的补充；检查超时的 Broker，把其从 BrokerList 中剔除；\n11 当 RouterPartitionNum 倍增时，Router 依据规则【RoomID % BrokerPartitionNumber == BrokerReplicaPartitionID % BrokerPartitionNumber】清洗自身路由信息缓存中数据；\n12 Router 本地存储每个 Gateway 的最大 GatewayMsgID，收到小于 GatewayMsgID的Gateway Message 可以丢弃不处理，否则就更新 GatewayMsgID 并根据上面逻辑进行处理。\n之所以把 Gateway Message 和 Heartbeat Message 放在一个线程处理，是为了防止进程假死这种情况。\nBroker 也采用了分 Partition 分 Replica 机制，所以向 Broker 转发 Gateway Message 时候路由规则，与 Gateway 向 Router 转发消息的路由规则相同。\n另外启动一个工具，当水平扩展后新启动的 Partition 内所有 Replica 的状态都是 Running 的时候，修改 Registry 路径 /pubsub/router/partition_num 的值为所有 Partition 的数目。\n5.3 Broker Broker 详细流程如下：\n1 Broker 加载配置，获取自身所在 Partition 的 ID（假设为3）；\n2 向 Registry 路径 /pubsub/broker/partition3 注册，设置其状态为 Init，注册中心返回的 ID 作为自身的 ID(replicaID)；\n3 从 Registry 路径 /pubsub/router/partition_num 获取当前有效的 Router Partition Number；\n4 从 Registry 路径 /pubsub/router/partition(x) 下获取每个 Router Partition 的各个replica；\n5 启动一个线程关注 Registry 路径 /pubsub/router，以实时获取以下信息：\nRouter Partition Number； 新的Router Partition（此时发生了扩容）； Partition内新的replica（Partition内发生了replica扩容）； Parition内某replica挂掉的信息；\n6 依据规则【RouterPartitionID % BrokerPartitionNum == BrokerPartitionID % BrokerPartitionNum，RouterReplicaID = BrokerReplicaID % BrokerPartitionNum】选定目标 Router Partition 下某个 Router replica，向其发送心跳消息，包含 BrokerPartitionNum、BrokerPartitionID、BrokerHostAddr 和精确到秒级的 Timestamp ，并异步等待所有 Router replica 的回复，所有 Router 转发来的 Gateway Message 放入 GatewayMessageQueue；\n7 依据规则【BrokerPartitionID == RoomID % BrokerParitionNum】从 Database 加载数据；\n8 依据规则【BrokerPartitionID % BrokerParitionNum == RoomID % BrokerParitionNum】异步处理 GatewayMessageQueue 内的 Gateway Message，只留下合乎规则的消息的数据；\n9 修改 Registry 路径 /pubsub/broker/partition3 下自身节点的状态为 Running；\n10 启动一个线程定时读取 Registry 路径 /pubsub/router 下各个子路径的值，以定时轮询的策略观察 Router 各 Partition 的变动情况，作为实时策略的补充；定时检查超时的 Router，某 Router 超时后更换其所在的 Partition 内其他 Router 替换之，定时发送心跳包；\n11 当 Registry 路径 /pubsub/broker/partition_num 的值 BrokerPartitionNum 发生改变的时候，依据规则【PartitionID == RoomID % PartitionNum】清洗本地路由信息缓存中每条数据；\n12 接收 Proxy 发来的 Room Message，依据 RoomID 从路由信息缓存中查找 Room 有成员登陆的所有 Gateway，把消息转发给这些 Gateway；\n13 Broker 本地存储每个 Gateway 的最大 GatewayMsgID，收到小于 GatewayMsgID 的 Gateway Message 可以丢弃不处理，否则更新 GatewayMsgID 并根据上面逻辑进行处理。\nBrokerPartitionNumber 可以小于或者等于或者大于 RouterPartitionNumber，两个数应该均是2的幂，两个集群可以分别进行扩展，互不影响。譬如BrokerPartitionNumber=4 而 RouterPartitionNumber=2，则 Broker Partition 3 只需要向 Router Partition 1 的某个 follower 发送心跳消息即可；若 BrokerPartitionNumber=4 而 RouterPartitionNumber=8，则 Broker Partition 3 需要向 Router Partition 3 的某个 follower 发送心跳消息的同时，还需要向 Router Partition 7 的某个 follower 发送心跳，以获取全量的 Gateway Message。\nBroker 需要关注 /pubsub/router/partition_num 和 /pubsub/broker/partition_num 的值的变化，当 router 或者 broker 进行 parition 水平扩展的时候，Broker 需要及时重新构建与 Router 之间的对应关系，及时变动发送心跳的 Router Replica 对象【RouterPartitionID = BrokerReplicaID % RouterPartitionNum，RouterPartitionID 为 Router Replica 在 PartitionRouterReplicaArray 数组的下标】。\n当 Router Partition 内 replica 死掉或者发送心跳包的 replica 对象死掉（无论是注册中心通知还是心跳包超时），broker 要及时变动发送心跳的 Router replica 对象。\n另外，Gateway 使用 UDP 通信方式向 Router 发送 Gateway Message，如若这个 Message 丢失则此 Gateway 上该 Room 内所有成员一段时间内（当有新的成员在当前 Gateway 上加入 Room\n时会产生新的 Gateway Message）都无法再接收消息，为了保证消息的可靠性，可以使用这样一个约束解决问题：**在此Gateway上登录的某 Room 内的人数少于 3 时，Gateway 会把 Gateway Message 复制两份非连续（如以10ms为时间间隔）重复发送给某个 Partition leader。**因 Gateway Message 消息处理的幂等性，重复 Gateway Message 并不会导致 Room Message 发送错误，只在极少概率的情况下会导致 Gateway 收到消息的时候 Room 内已经没有成员在此 Gateway 登录，此时 Gateway 会把消息丢弃不作处理。\n传递实时消息群聊消息系统的 Broker 向特定 Gateway 转发 Room Message 的时候，会带上 Room 内在此 Gateway 上登录的用户列表，Gateway 根据这个用户列表下发消息时如果检测到此用户已经下线，在放弃向此用户转发消息的同时，还应该把此用户已经下线的消息发送给 Router，当 Router 把这个消息转发给 Broker 后，Broker 把此用户从用户列表中剔除。通过这种负反馈机制保证用户状态更新的及时性。\n6 离线消息 前期的系统只考虑了用户在线情况下实时消息的传递，当用户离线时其消息便无法获取。若系统考虑用户离线消息传递，需要考虑如下因素：\n消息固化：保证用户上线时收到其离线期间的消息； 消息有序：离线消息和在线消息都在一个消息系统传递，给每个消息分配一个 ID 以区分消息先后顺序，消息顺序越靠后则 ID 愈大。 离线消息的存储和传输，需要考虑用户的状态以及每条消息的发送状态，整个消息核心链路流程会有大的重构。新消息架构如下图：\n系统名词解释：\n1 Pi : 消息ID存储模块，存储每个人未发送的消息 ID 有序递增集合；\n2 Xiu : 消息存储KV模块，存储每个人的消息，给每个消息分配 ID，以 ID 为 key，以消息内为 value；\n3 Gateway Message(HB) : 用户登录登出消息，包括APP保活定时心跳（Hearbeat）消息；\n系统内部代号貔貅(貔貅者，雄貔雌貅)，源自上面两个新模块。\n这个版本架构流程的核心思想为“消息ID与消息内容分离，消息与用户状态分离”。消息发送流程涉及到模块 Client/Proxy/Pi/Xiu，消息推送流程则涉及到模块 Pi/Xiu/Broker/Router/Gateway。\n下面先细述Pi和Xiu的接口，然后再详述发送和推送流程。\n6.1 Xiu Xiu 模块功能名称是 Message Storage，用户缓存和固化消息，并给消息分配 ID。Xiu 集群采用分 Partition 分 Replica 机制，Partition 初始数目须是2的倍数，集群扩容时采用翻倍法。\n6.1.1 存储消息 存储消息请求的参数列表为 {SnowflakeID，UIN, Message}，其流程如下：\n1 接收客户端发来的消息，获取消息接收人 ID（UIN）和客户端给消息分配的 SnowflakeID；\n2 检查 UIN % Xiu_Partition_Num == Xiu_Partition_ID % Xiu_Partition_Num 添加是否成立【即接收人的消息是否应当由当前Xiu负责】，不成立则返回错误并退出；\n3 检查 SnowflakeID 对应的消息是否已经被存储过，若已经存储过则返回其对应的消息ID然后退出；\n4 给消息分配一个 MsgID；\n每个Xiu有自己唯一的 Xiu_Partition_ID，以及一个初始值为 0 的 Partition_Msg_ID。MsgID = 1B[ Xiu_Partition_ID ] + 1B[ Message Type ] + 6B[ ++ Partition_Msg_ID ]。每次分配的时候 Partition_Msg_ID 都自增加一。\n5 以 MsgID 为 key 把消息存入基于共享内存的 Hashtable，并存入消息的 CRC32 hash值和插入时间，把 MsgID 存入一个 LRU list 中；\nLRU List 自身并不存入共享内存中，当进程重启时，可以根据Hashtable中的数据重构出这个List。把消息存入 Hashtable 中时，如果 Hashtable full，则依据 LRU List 对Hashtable 中的消息进行淘汰。\n6 把MsgID返回给客户端；\n7 把MsgID异步通知给消息固化线程，消息固化线程根据MsgID从Hashtable中读取消息并根据CRC32 hash值判断消息内容是否完整，完整则把消息存入本地RocksDB中；\n6.1.2 读取消息 读取消息请求的参数列表为{UIN, MsgIDList}，其流程为：\n1 获取请求的 MsgIDList，判断每个MsgID MsgID{Xiu_Partition_ID} == Xiu_Partition_ID 条件是否成立，不成立则返回错误并退出； 2 从 Hashtable 中获取每个 MsgID 对应的消息； 3 如果 Hashtable 中不存在，则从 RocksDB 中读取 MsgID 对应的消息； 4 读取完毕则把所有获取的消息返回给客户端。 6.1.3 主从数据同步 目前从简，暂定 Xiu 的副本只有一个。\nXiu 节点启动的时候根据自身配置文件中分配的 Xiu_Partition_ID 到 Registry 路径 /pubsub/xiu/partition_id 下进行注册一个临时有序节点，注册成功则 Registry 会返回 Xiu 的节点 ID。\nXiu节点获取 /pubsub/xiu/partition\\_id 下的所有节点的 ID 和地址信息，依据 节点ID最小者为leader 的原则，即可判定自己的角色。只有 leader 可接受读写数据请求。\n数据同步流程如下：\n1 follower 定时向 leader 发送心跳信息，心跳信息包含本地最新消息的 ID； 2 leader 启动一个数据同步线程处理 follower 的心跳信息，leader 的数据同步线程从 LRU list 中查找 follower_latest_msg_id 之后的N条消息的ID，若获取到则读取消息并同步给 follower，获取不到则回复其与 leader 之间消息差距太大； 3 follower 从 leader 获取到最新一批消息，则存储之； 4 follower 若获取 leader 的消息差距太大响应，则请求 leader 的 agent 把 RocksDB 的固化数据全量同步过来，整理完毕后再次启动与 leader 之间的数据同步流程。 follower 会关注 Registry 路径 /pubsub/xiu/partition_id 下所有所有节点的变化情况，如果 leader 挂掉则及时转换身份并接受客户端请求。如果 follower 与 leader 之间的心跳超时，则 follower 删掉 leader 的 Registry 路径节点，及时进行身份转换处理客户端请求。\n当 leader 重启或者 follower 转换为 leader 的时候，需要把 Partition_Msg_ID 进行一个大数值增值（譬如增加1000）以防止可能的消息 ID 乱序情况。\n6.1.4 集群扩容 Xiu 集群扩容采用翻倍法，扩容时新 Partition 的节点启动后工作流程如下：\n1 向Registry的路径 /pubsub/xiu/partition_id 下自己的 node 的 state 为 running，同时注册自己的对外服务地址信息； 另外启动一个工具，当水平扩展后所有新启动的 Partition 内所有 Replica 的状态都是 Running 的时候，修改 Registry 路径 /pubsub/xiu/partition_num 的值为扩容后 Partition 的数目。按照开头的例子，即由2升级为4。\n之所以 Xiu 不用像 Broker 和 Router 那样启动的时候向老的 Partition 同步数据，是因为每个 Xiu 分配的 MsgID 中已经带有 Xiu 的 PartitionID 信息，即使集群扩容这个 ID 也不变，根据这个ID也可以定位到其所在的Partition，而不是借助 hash 方法。\n6.2 Pi Pi 模块功能名称是 Message ID Storage，存储每个用户的 MsgID List。Xiu 集群也采用分 Partition 分 Replica 机制，Partition 初始数目须是2的倍数，集群扩容时采用翻倍法。\n6.2.1 存储消息ID MsgID 存储的请求参数列表为{UIN，MsgID}，Pi 工作流程如下：\n1 判断条件 UIN % Pi_Partition_Num == Pi_Partition_ID % Pi_Partition_Num 是否成立，若不成立则返回error退出； 2 把 MsgID 插入UIN的 MsgIDList 中，保持 MsgIDList 中所有 MsgID 不重复有序递增，把请求内容写入本地log，给请求者返回成功响应。 Pi有专门的日志记录线程，给每个日志操作分配一个 LogID，每个 Log 文件记录一定量的写操作，当文件 size 超过配置的上限后删除之。\n6.2.2 读取消息ID列表 读取请求参数列表为{UIN, StartMsgID, MsgIDNum, ExpireFlag}，其意义为获取用户 UIN 自起始ID为 StartMsgID 起（不包括 StartMsgID ）的数目为 MsgIDNum 的消息ID列表，ExpireFlag意思是 所有小于等于 StartMsgID 的消息ID是否删除。 流程如下：\n1 判断条件 UIN % Pi_Partition_Num == Pi_Partition_ID % Pi_Partition_Num 是否成立，若不成立则返回error退出； 2 获取 (StartID, StartMsgID + MsgIDNum] 范围内的所有 MsgID，把结果返回给客户端； 3 如果 ExpireFlag 有效，则删除MsgIDList内所有在 [0, StartMsgID] 范围内的MsgID，把请求内容写入本地log。 6.2.3 主从数据同步 同 Xiu 模块，暂定 Pi 的同 Parition 副本只有一个。\nPi 节点启动的时候根据自身配置文件中分配的 Pi_Partition_ID 到Registry路径 /pubsub/pi/partition_id 下进行注册一个临时有序节点，注册成功则 Registry 会返回 Pi 的节点 ID。\nPi 节点获取 /pubsub/pi/partition_id 下的所有节点的ID和地址信息，依据 节点ID最小者为leader 的原则，即可判定自己的角色。只有 leader 可接受读写数据请求。\n数据同步流程如下：\n1 follower 定时向 leader 发送心跳信息，心跳信息包含本地最新 LogID； 2 leader 启动一个数据同步线程处理 follower 的心跳信息，根据 follower 汇报的 logID 把此 LogID； 3 follower 从 leader 获取到最新一批 Log，先存储然后重放。 follower 会关注 Registry 路径 /pubsub/pi/partition_id 下所有节点的变化情况，如果 leader 挂掉则及时转换身份并接受客户端请求。如果follower 与 leader 之间的心跳超时，则follower删掉 leader 的 Registry 路径节点，及时进行身份转换处理客户端请求。\n6.2.4 集群扩容 Pi 集群扩容采用翻倍法。则节点启动后工作流程如下：\n1 向 Registry 注册，获取 Registry 路径 /pubsub/xiu/partition_num 的值 PartitionNumber； 2 如果发现自己 PartitionID 满足条件 PartitionID \u0026gt;= PartitionNumber 时，则意味着当前 Partition 是扩容后的新集群，更新 Registry 中自己状态为start； 3 读取 Registry 路径 /pubsub/xiu 下所有 Parition 的 leader，根据条件 自身PartitionID % PartitionNumber == PartitionID % PartitionNumber 寻找对应的老 Partition 的 leader，称之为 parent_leader； 4 缓存收到 Proxy 转发来的用户请求； 5 向 parent_leader 获取log； 6 向 parent_leader 同步内存数据； 7 重放 parent_leader 的log； 8 更新 Registry 中自己的状态为 Running； 9 重放用户请求； 10 当 Registry 路径 /pubsub/xiu/partition_num 的值 PartitionNumber 满足条件 PartitionID \u0026gt;= PartitionNumber 时，意味着扩容完成，处理用户请求时要给用户返回响应。 Proxy 会把读写请求参照条件 UIN % Pi\\_Partition\\_Num == Pi\\_Partition\\_ID % Pi\\_Partition\\_Num 向相关 partition 的 leader 转发用户请求。假设原来 PartitionNumber 值为2，扩容后值为4，则原来转发给 partition0 的写请求现在需同时转发给 partition0 和 partition2，原来转发给 partition1 的写请求现在需同时转发给 partition1 和 partition3。\n另外启动一个工具，当水平扩展后所有新启动的 Partition 内所有 Replica 的状态都是 Running 的时候，修改Registry路径/pubsub/xiu/partition_num的值为扩容后 Partition 的数目。\n6.3 数据发送流程 消息自 PiXiu 的外部客户端（Client，服务端所有使用 PiXiu 提供的服务者统称为客户端）按照一定负载均衡规则发送到 Proxy，然后存入 Xiu 中，把 MsgID 存入 Pi 中。其详细流程如下：\n1 Client 依据 snowflake 算法给消息分配 SnowflakeID，依据 ProxyID = UIN % ProxyNum 规则把消息发往某个 Proxy； 2 Proxy 收到消息后转发到 Xiu； 3 Proxy 收到 Xiu 返回的响应后，把响应转发给 Client； 4 如果 Proxy 收到 Xiu 返回的响应带有 MsgID，则发起 Pi 写流程，把 MsgID 同步到 Pi 中； 5 如果 Proxy 收到 Xiu 返回的响应带有 MsgID，则给 Broker 发送一个 Notify，告知其某 UIN 的最新 MsgID。 6.4 数据转发流程 转发消息的主体是Broker，原来的在线消息转发流程是它收到 Proxy 转发来的 Message，然后根据用户是否在线然后转发给 Gateway。\nPiXiu架构下 Broker 会收到以下类型消息：\n用户登录消息 用户心跳消息 用户登出消息 Notify 消息 Ack 消息 Broker流程受这五种消息驱动，下面分别详述其收到这五种消息时的处理流程。\n用户登录消息流程如下：\n1 检查用户的当前状态，若为 OffLine 则把其状态值为在线 OnLine； 2 检查用户的待发送消息队列是否为空，不为空则退出； 3 向 Pi 模块发送获取 N 条消息 ID 的请求 {UIN: uin, StartMsgID: 0, MsgIDNum: N, ExpireFlag: false}，设置用户状态为 GettingMsgIDList 并等待回应； 4 根据 Pi 返回的消息 ID 队列，向 Xiu 发起获取消息请求 {UIN: uin, MsgIDList: msg ID List}，设置用户状态为 GettingMsgList 并等待回应； 5 Xiu 返回消息列表后，设置状态为 SendingMsg，并向 Gateway 转发消息。 可以把用户心跳消息当做用户登录消息处理。\nGateway的用户登出消息产生有三种情况：\n1 用户主动退出； 2 用户心跳超时； 3 给用户转发消息时发生网络错误； 用户登出消息处理流程如下：\n1 检查用户状态，如果为 OffLine，则退出； 2 用户状态不为 OffLine 且检查用户已经发送出去的消息列表的最后一条消息的 ID（LastMsgID），向 Pi 发送获取 MsgID 请求{UIN: uin, StartMsgID: LastMsgID, MsgIDNum: 0, ExpireFlag: True}，待 Pi 返回响应后退出； 处理 Proxy 发来的 Notify 消息处理流程如下：\n1 如果用户状态为 OffLine，则退出； 2 更新用户的最新消息 ID（LatestMsgID），如果用户发送消息队列不为空则退出； 3 向 Pi 模块发送获取 N 条消息 ID 的请求 {UIN: uin, StartMsgID: 0, MsgIDNum: N, ExpireFlag: false}，设置用户状态为 GettingMsgIDList 并等待回应； 4 根据 Pi 返回的消息 ID 队列，向 Xiu 发起获取消息请求 {UIN: uin, MsgIDList: msg ID List}，设置用户状态为 GettingMsgList 并等待回应； 5 Xiu 返回消息列表后，设置状态为 SendingMsg，并向 Gateway 转发消息。 所谓 Ack 消息，就是 Broker 经 Gateway 把消息转发给 App 后，App 给Broker的消息回复，告知Broker其最近成功收到消息的 MsgID。\nAck 消息处理流程如下：\n1 如果用户状态为 OffLine，则退出； 2 更新 LatestAckMsgID 的值； 3 如果用户发送消息队列不为空，则发送下一个消息后退出； 4 如果 LatestAckMsgID \u0026gt;= LatestMsgID，则退出； 5 向 Pi 模块发送获取 N 条消息 ID 的请求 {UIN: uin, StartMsgID: 0, MsgIDNum: N, ExpireFlag: false}，设置用户状态为 GettingMsgIDList 并等待回应； 6 根据 Pi 返回的消息 ID 队列，向 Xiu 发起获取消息请求 {UIN: uin, MsgIDList: msg ID List}，设置用户状态为 GettingMsgList 并等待回应； 7 Xiu 返回消息列表后，设置状态为 SendingMsg，并向 Gateway 转发消息。 总体上，PiXiu 转发消息流程采用拉取（pull）转发模型，以上面五种消息为驱动进行状态转换，并作出相应的动作行为。\n7 单聊消息 前几章所述之系统架构均基于群聊这一实时通信场景，此群聊消息系统在公司内部稳定运行近一年半时间，期间经历了6个大版本的迭代演进，赢得了各个业务线同事的信任，承担了从群聊、聊天室、语音传递、到游戏信令传递等等各种场景的群聊实时通信业务。\n2017 年 9 月份刚接手系统时，已有业务方提出把对单人下发的消息【以下称之为 Chat Message】也接入这套系统，即消息系统不仅是一个群聊消息下发通道，也应该是一个单聊消息下发通道，但是考虑到当时处于系统实现初期，个人基于系统小作的考量直接拒绝了。随着这一年半其接入的业务范围的扩展，目下公司正考虑拆分 Gateway，这套消息系统需要被重构以处理单聊消息就是势然了。\n公司的 Gateway 大概是我见过的最复杂的接口层系统，各种本应该放在逻辑层处理的子系统都被糅合进了接口层 Gateway 这一单个模块之内，除了公司几个创始人外很少有人清楚其它内部逻辑，据说陆续有四个高薪招聘的高手在接手这套系统后三个月内跑路了。Gateway 内部与聊天场景有关的状态数据主要有两种：用户所在的群信息【以下简称 Router Data】以及用户自身的状态信息【包括在线状态以及客户端地址信息，以下简称 Relay Data】。\nRouter Data 已经被群聊系统的 Router 模块替代掉，当下的 Gateway 已经不需要存储 Router Data。若扩展此系统使其能够处理单聊消息，能够存储用户的在线状态以及用户接入的 Gateway 信息，则 Gateway 就可以不用存储任何有关用户的状态数据了。\n7.1 实时消息系统架构 Relay Data 迥异于 Router Data：Relay Data 的 key 是 UIN，其数据传递依赖于 Relay Message；而 Router Data 的 key 是 GroupID，其数据传递依赖于 Gateway Message。此二者差异决定了这两种数据的处理不可能在一个单一的模块之内。\n新的实时通信系统添加了一个 Relay 模块用于处理 Relay Message 并存储 Relay Data。添加了单聊消息处理能力的实时消息系统架构如下：\n下面详述新架构下各个模块的功能和消息处理流程，至于系统注册、系统扩容以及注册通知等相关流程与以往处理机制雷同，下面不再详述。\n注意：新架构图中把原来的专门存储 Router Data 的 Database 模块改称为 Router DB，并添加了一个 Relay DB，以专门存储 Relay Data。\n7.2 Gateway Gateway 不再存储 Router Data 和 Relay Data，几乎成了 APP 的透明代理，其功能如下：\n接收 APP 的连接请求，并把用户连接消息以 Relay Message 形式发送给 Relay；APP 与 Gateway 连接断开时以 Relay Message 形式发送给 Relay；把用户登录登出某 Room 的消息转发给 Router；接收 Relay 转发来的 Room Message 和 Chat Message，并下发给 APP。\n7.3 Relay Relay 是一个新模块，但其组织方式类似于 Router，亦是分 Partition 分 Replica，处理 Relay Message。\nRelay 模块依据用户的 UIN 进行把不同用户的 Relay Data 放入不同的 Parition，同 Partition 内的 所有 Relay Replica 数据一致。\nRelay 功能列表如下：\n接收由 Gateway 转发来的 Relay Message，存储为 Relay Data，并把 Relay Data 异步存入 Relay DB；起始时先从 Relay DB 获取其 Partiton 内所有用户的 Relay Data，以与 Partiton 内其他 Replica 保持数据一致性；接收由 Broker 转发来的 Room Message，依据 Relay Data 记录的用户所在的 Gateway 把消息复制转发给 Gateway；接收由 Proxy 转发来的 Chat Message，依据 Relay Data 记录的用户所在的 Gateway 把消息复制转发给 Gateway；\n前面提及所有处理 Room Message 系统都是一个写放大的系统，究其原因是一条 Room Message 需要被复制下发给 Room 内每个人，所以 Relay 处理 Room Message 时需要根据 Room Message 内的 Room UIN List 对消息进行复制后下发给每个 User APP 所连接的 Gateway。\n当然，如果用户不在线，Relay 会对 Room Message 和 Chat Message 都作丢弃处理，因为此系统只处理实时消息。\nRelay 承担了群聊消息与单聊消息的下发。\n7.4 Broker 新架构下 Broker 模块不再直接把 Room Message 转发给 Gateway，而是转发给 Relay。有了 Relay，Broker 自身只需要存储每个 Group 的 UIN List 即可，大大减轻了自身的任务流程，Broker 可以把 Relay 视作 pseudo-gateway。\nBroker 功能列表如下：\n接收 Router 转发来的 Gateway Message，存储为 Router Data，并把 Router Data 异步存入 Router DB；起始时先从 Router DB 获取其 Partiton 内所有用户的 Router Data，以与 Partiton 内其他 Replica 保持数据一致性；接收由 Proxy 转发来的 Room Message，依据 Router Data 记录的 Group 内的 UIN List 把消息复制转发给各个 Relay；\nBroker 仅仅承担了群聊消息下发。\n7.4 Proxy Proxy 接收 Client 发来的消息时，需要区分消息是 Room Message 还是 Chat Message，新架构下其功能列表如下：\n关注 Registry Broker Path，以实时获取正确的 Broker List；关注 Registry Relay Path，以实时获取正确的 Relya List；接收 Client 发来的 Room Message，依据 Group 把消息转发给 Broker；接收 Client 发来的 Chat Message，依据 UIN 把消息转发给 Relay；\nProxy 对 Group Message 的处理仅仅是转发了事，并不需要复制，所以不存在写放大的问题。\n其实还可以把 Proxy 区分为 Group Message Proxy 和 Chat Message Proxy，以期收逻辑清晰职责明了之效。\n8 总结 这套群聊消息系统尚有以下task list需完善：\n1 消息以UDP链路传递，不可靠【2018/01/29解决之】； 2 目前的负载均衡算法采用了极简的RoundRobin算法，可以根据成功率和延迟添加基于权重的负载均衡算法实现； 3 只考虑传递，没有考虑消息的去重，可以根据消息ID实现这个功能【2018/01/29解决之】； 4 各个模块之间没有考虑心跳方案，整个系统的稳定性依赖于Registry【2018/01/17解决之】； 5 离线消息处理【2018/03/03解决之】； 6 区分消息优先级； 7 添加 Metaserver 模块，监控各 Broker 的 metrics数据； 8 添加虚拟节点和物理节点之间的映射关系，把翻倍扩容方式升级为冷热负载均衡迁移式的动态扩缩容方式； ","permalink":"https://asterzephyr.github.io/posts/-im/","summary":"\u003ch1 id=\"高可用im\"\u003e高可用IM\u003c/h1\u003e\n\u003cp\u003eCreated: 2025年1月19日 18:15\nStatus: 完成\u003c/p\u003e\n\u003ch1 id=\"一套高可用实时消息系统实现\"\u003e\u003cstrong\u003e一套高可用实时消息系统实现\u003c/strong\u003e\u003c/h1\u003e\n\u003chr\u003e\n\u003ch3 id=\"实时消息系统与消息队列系统\"\u003e\u003cstrong\u003e实时消息系统与消息队列系统\u003c/strong\u003e\u003c/h3\u003e\n\u003chr\u003e\n\u003cp\u003e实时消息【即时通信】系统，有群聊和单聊两种方式，其形态异于消息队列：\u003c/p\u003e\n\u003cul\u003e\n\u003cli\u003e\n\u003cp\u003e1 大量的 group 信息变动\u003c/p\u003e\n\u003cp\u003e群聊形式的即时通信系统在正常服务形态下，瞬时可能有大量用户登入登出，而消息队列系统 producer/consumer group 内成员变动相对静止。\u003c/p\u003e","title":"高可用IM"},{"content":"IO密集型和CPU密集型业务占比 Created: 2025年1月30日 02:50 Status: 完成\n一、I/O密集型任务的主导场景 1. 高并发用户请求处理 典型业务：电商平台、社交网络、在线教育、新闻门户。 核心任务： HTTP API响应：处理用户登录、商品查询、订单提交等请求，涉及数据库读写（MySQL/MongoDB）、缓存访问（Redis/Memcached）。 实时消息推送：聊天应用（如微信）、通知系统（如邮件/短信）依赖WebSocket或长轮询，需高频网络通信。 技术挑战：应对C10K问题，需通过异步框架（Node.js、Go）或事件驱动模型（Nginx）提升吞吐量。 2. 数据存储与检索 典型业务：内容平台（如短视频）、云存储服务（如阿里云OSS）。 核心任务： 文件读写：用户上传图片/视频至对象存储，分发时通过CDN加速。 数据库操作：社交媒体的动态加载、评论查询涉及分库分表和索引优化。 技术工具：分布式数据库（TiDB）、NoSQL（Cassandra）、搜索引擎（Elasticsearch）。 3. 微服务间通信 典型业务：微服务架构的金融系统、在线旅游平台。 核心任务： RPC调用：服务间通过gRPC/Dubbo同步数据。 消息队列：使用Kafka/RabbitMQ实现异步削峰和解耦。 性能瓶颈：网络延迟和序列化/反序列化效率。 4. 实时数据流处理 典型业务：物联网（IoT）、金融交易监控。 核心任务： 流式计算：Flink/Spark Streaming处理传感器数据或交易日志。 低延迟响应：风控系统需在毫秒级完成反欺诈计算。 二、CPU密集型任务的主要分布 1. 数据科学与机器学习 典型业务：推荐系统（如抖音算法）、广告投放（如精准CTR预测）。 核心任务： 模型训练：使用TensorFlow/PyTorch进行大规模数据训练（GPU加速）。 实时推理：图像识别（如人脸验证）、自然语言处理（如智能客服）。 资源需求：依赖GPU集群和分布式计算框架（Horovod）。 2. 多媒体处理 典型业务：视频平台（如YouTube）、直播应用（如Twitch）。 核心任务： 视频转码：H.264/H.265编码转换（FFmpeg）。 图像渲染：游戏云服务（如GeForce NOW）的实时画面生成。 技术痛点：计算资源密集，需优化并行算法。 3. 加密与安全计算 典型业务：区块链（如以太坊）、支付系统（如支付宝）。 核心任务： 加密运算：非对称加密（RSA）、哈希计算（SHA-256）。 零知识证明：隐私保护场景下的复杂数学运算。 硬件依赖：部分场景需专用硬件（如SGX）。 4. 复杂业务逻辑处理 典型业务：3D设计软件（如AutoCAD）、仿真系统（如ANSYS）。 核心任务： 物理引擎计算：游戏中的碰撞检测、流体模拟。 数值分析：金融衍生品定价模型（蒙特卡洛模拟）。 三、任务类型占比分析 1. 互联网业务场景占比 任务类型 占比 典型场景举例 I/O密集型 70% API请求、数据库操作、消息队列 CPU密集型 25% 机器学习推理、视频转码、加密计算 混合型任务 5% 实时数据分析（如Flink窗口计算） 2. 趋势变化 I/O密集型增长点： 物联网设备爆发（2025年预计全球750亿台设备联网）。 实时交互应用普及（元宇宙、VR社交）。 CPU密集型增长点： AI工业化落地（AIGC、自动驾驶）。 量子计算突破带来的新型计算需求。 四、技术选型建议 1. I/O密集型场景优化 架构设计： 异步非阻塞框架（Netty、Tornado）。 缓存分层策略（本地缓存+分布式缓存）。 基础设施： 使用RDMA网络降低延迟。 部署NVMe SSD提升存储IOPS。 2. CPU密集型场景优化 计算加速： GPU/TPU异构计算（CUDA、OpenCL）。 分布式任务调度（Kubernetes批处理任务）。 代码级优化： SIMD指令集（AVX-512）。 JIT编译（PyPy、Numba）。 五、总结 当前互联网业务中，I/O密集型任务占据主导地位（约70%），集中在高并发请求处理、数据存储与微服务通信；而CPU密集型任务（约25%）则集中在AI、多媒体和安全领域。随着边缘计算和AI技术的普及，未来两类任务将更深度耦合（如端侧AI推理需同时优化I/O和计算），技术选型需兼顾灵活性与性能。\n","permalink":"https://asterzephyr.github.io/posts/io-cpu-/","summary":"\u003ch1 id=\"io密集型和cpu密集型业务占比\"\u003eIO密集型和CPU密集型业务占比\u003c/h1\u003e\n\u003cp\u003eCreated: 2025年1月30日 02:50\nStatus: 完成\u003c/p\u003e\n\u003ch3 id=\"一io密集型任务的主导场景\"\u003e\u003cstrong\u003e一、I/O密集型任务的主导场景\u003c/strong\u003e\u003c/h3\u003e\n\u003ch3 id=\"1-高并发用户请求处理\"\u003e\u003cstrong\u003e1. 高并发用户请求处理\u003c/strong\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e典型业务\u003c/strong\u003e：电商平台、社交网络、在线教育、新闻门户。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e核心任务\u003c/strong\u003e：\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eHTTP API响应\u003c/strong\u003e：处理用户登录、商品查询、订单提交等请求，涉及数据库读写（MySQL/MongoDB）、缓存访问（Redis/Memcached）。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e实时消息推送\u003c/strong\u003e：聊天应用（如微信）、通知系统（如邮件/短信）依赖WebSocket或长轮询，需高频网络通信。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e技术挑战\u003c/strong\u003e：应对C10K问题，需通过异步框架（Node.js、Go）或事件驱动模型（Nginx）提升吞吐量。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"2-数据存储与检索\"\u003e\u003cstrong\u003e2. 数据存储与检索\u003c/strong\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e典型业务\u003c/strong\u003e：内容平台（如短视频）、云存储服务（如阿里云OSS）。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e核心任务\u003c/strong\u003e：\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e文件读写\u003c/strong\u003e：用户上传图片/视频至对象存储，分发时通过CDN加速。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e数据库操作\u003c/strong\u003e：社交媒体的动态加载、评论查询涉及分库分表和索引优化。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e技术工具\u003c/strong\u003e：分布式数据库（TiDB）、NoSQL（Cassandra）、搜索引擎（Elasticsearch）。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"3-微服务间通信\"\u003e\u003cstrong\u003e3. 微服务间通信\u003c/strong\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e典型业务\u003c/strong\u003e：微服务架构的金融系统、在线旅游平台。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e核心任务\u003c/strong\u003e：\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eRPC调用\u003c/strong\u003e：服务间通过gRPC/Dubbo同步数据。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e消息队列\u003c/strong\u003e：使用Kafka/RabbitMQ实现异步削峰和解耦。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e性能瓶颈\u003c/strong\u003e：网络延迟和序列化/反序列化效率。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"4-实时数据流处理\"\u003e\u003cstrong\u003e4. 实时数据流处理\u003c/strong\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e典型业务\u003c/strong\u003e：物联网（IoT）、金融交易监控。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e核心任务\u003c/strong\u003e：\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e流式计算\u003c/strong\u003e：Flink/Spark Streaming处理传感器数据或交易日志。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e低延迟响应\u003c/strong\u003e：风控系统需在毫秒级完成反欺诈计算。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch3 id=\"二cpu密集型任务的主要分布\"\u003e\u003cstrong\u003e二、CPU密集型任务的主要分布\u003c/strong\u003e\u003c/h3\u003e\n\u003ch3 id=\"1-数据科学与机器学习\"\u003e\u003cstrong\u003e1. 数据科学与机器学习\u003c/strong\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e典型业务\u003c/strong\u003e：推荐系统（如抖音算法）、广告投放（如精准CTR预测）。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e核心任务\u003c/strong\u003e：\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e模型训练\u003c/strong\u003e：使用TensorFlow/PyTorch进行大规模数据训练（GPU加速）。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e实时推理\u003c/strong\u003e：图像识别（如人脸验证）、自然语言处理（如智能客服）。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e资源需求\u003c/strong\u003e：依赖GPU集群和分布式计算框架（Horovod）。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"2-多媒体处理\"\u003e\u003cstrong\u003e2. 多媒体处理\u003c/strong\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e典型业务\u003c/strong\u003e：视频平台（如YouTube）、直播应用（如Twitch）。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e核心任务\u003c/strong\u003e：\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e视频转码\u003c/strong\u003e：H.264/H.265编码转换（FFmpeg）。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e图像渲染\u003c/strong\u003e：游戏云服务（如GeForce NOW）的实时画面生成。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e技术痛点\u003c/strong\u003e：计算资源密集，需优化并行算法。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"3-加密与安全计算\"\u003e\u003cstrong\u003e3. 加密与安全计算\u003c/strong\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e典型业务\u003c/strong\u003e：区块链（如以太坊）、支付系统（如支付宝）。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e核心任务\u003c/strong\u003e：\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e加密运算\u003c/strong\u003e：非对称加密（RSA）、哈希计算（SHA-256）。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e零知识证明\u003c/strong\u003e：隐私保护场景下的复杂数学运算。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e硬件依赖\u003c/strong\u003e：部分场景需专用硬件（如SGX）。\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"4-复杂业务逻辑处理\"\u003e\u003cstrong\u003e4. 复杂业务逻辑处理\u003c/strong\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e典型业务\u003c/strong\u003e：3D设计软件（如AutoCAD）、仿真系统（如ANSYS）。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e核心任务\u003c/strong\u003e：\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e物理引擎计算\u003c/strong\u003e：游戏中的碰撞检测、流体模拟。\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e数值分析\u003c/strong\u003e：金融衍生品定价模型（蒙特卡洛模拟）。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch3 id=\"三任务类型占比分析\"\u003e\u003cstrong\u003e三、任务类型占比分析\u003c/strong\u003e\u003c/h3\u003e\n\u003ch3 id=\"1-互联网业务场景占比\"\u003e\u003cstrong\u003e1. 互联网业务场景占比\u003c/strong\u003e\u003c/h3\u003e\n\u003ctable\u003e\n  \u003cthead\u003e\n      \u003ctr\u003e\n          \u003cth\u003e\u003cstrong\u003e任务类型\u003c/strong\u003e\u003c/th\u003e\n          \u003cth\u003e\u003cstrong\u003e占比\u003c/strong\u003e\u003c/th\u003e\n          \u003cth\u003e\u003cstrong\u003e典型场景举例\u003c/strong\u003e\u003c/th\u003e\n      \u003c/tr\u003e\n  \u003c/thead\u003e\n  \u003ctbody\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eI/O密集型\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003e70%\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003eAPI请求、数据库操作、消息队列\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003eCPU密集型\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003e25%\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e机器学习推理、视频转码、加密计算\u003c/td\u003e\n      \u003c/tr\u003e\n      \u003ctr\u003e\n          \u003ctd\u003e\u003cstrong\u003e混合型任务\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e\u003cstrong\u003e5%\u003c/strong\u003e\u003c/td\u003e\n          \u003ctd\u003e实时数据分析（如Flink窗口计算）\u003c/td\u003e\n      \u003c/tr\u003e\n  \u003c/tbody\u003e\n\u003c/table\u003e\n\u003ch3 id=\"2-趋势变化\"\u003e\u003cstrong\u003e2. 趋势变化\u003c/strong\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003eI/O密集型增长点\u003c/strong\u003e：\n\u003cul\u003e\n\u003cli\u003e物联网设备爆发（2025年预计全球750亿台设备联网）。\u003c/li\u003e\n\u003cli\u003e实时交互应用普及（元宇宙、VR社交）。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003eCPU密集型增长点\u003c/strong\u003e：\n\u003cul\u003e\n\u003cli\u003eAI工业化落地（AIGC、自动驾驶）。\u003c/li\u003e\n\u003cli\u003e量子计算突破带来的新型计算需求。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch3 id=\"四技术选型建议\"\u003e\u003cstrong\u003e四、技术选型建议\u003c/strong\u003e\u003c/h3\u003e\n\u003ch3 id=\"1-io密集型场景优化\"\u003e\u003cstrong\u003e1. I/O密集型场景优化\u003c/strong\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e架构设计\u003c/strong\u003e：\n\u003cul\u003e\n\u003cli\u003e异步非阻塞框架（Netty、Tornado）。\u003c/li\u003e\n\u003cli\u003e缓存分层策略（本地缓存+分布式缓存）。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e基础设施\u003c/strong\u003e：\n\u003cul\u003e\n\u003cli\u003e使用RDMA网络降低延迟。\u003c/li\u003e\n\u003cli\u003e部署NVMe SSD提升存储IOPS。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003ch3 id=\"2-cpu密集型场景优化\"\u003e\u003cstrong\u003e2. CPU密集型场景优化\u003c/strong\u003e\u003c/h3\u003e\n\u003cul\u003e\n\u003cli\u003e\u003cstrong\u003e计算加速\u003c/strong\u003e：\n\u003cul\u003e\n\u003cli\u003eGPU/TPU异构计算（CUDA、OpenCL）。\u003c/li\u003e\n\u003cli\u003e分布式任务调度（Kubernetes批处理任务）。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003cli\u003e\u003cstrong\u003e代码级优化\u003c/strong\u003e：\n\u003cul\u003e\n\u003cli\u003eSIMD指令集（AVX-512）。\u003c/li\u003e\n\u003cli\u003eJIT编译（PyPy、Numba）。\u003c/li\u003e\n\u003c/ul\u003e\n\u003c/li\u003e\n\u003c/ul\u003e\n\u003chr\u003e\n\u003ch3 id=\"五总结\"\u003e\u003cstrong\u003e五、总结\u003c/strong\u003e\u003c/h3\u003e\n\u003cp\u003e当前互联网业务中，\u003cstrong\u003eI/O密集型任务占据主导地位\u003c/strong\u003e（约70%），集中在高并发请求处理、数据存储与微服务通信；而\u003cstrong\u003eCPU密集型任务\u003c/strong\u003e（约25%）则集中在AI、多媒体和安全领域。随着边缘计算和AI技术的普及，未来两类任务将更深度耦合（如端侧AI推理需同时优化I/O和计算），技术选型需兼顾灵活性与性能。\u003c/p\u003e","title":"IO密集型和CPU密集型业务占比"}]