diff --git a/topics/index.json b/topics/index.json index d1eebf1..71a742c 100644 --- a/topics/index.json +++ b/topics/index.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "updated": "2026-09-07", + "updated": "2026-09-09", "topics": [ { "slug": "qunar-ai-fullstack", @@ -339,6 +339,176 @@ } } ] + }, + { + "slug": "interview-prep", + "name": "面试准备", + "description": "面试第一轮系统性复习:覆盖语言并发模型、分布式微服务、数据库进阶、消息队列、K8s与可观测性、AI工程实践", + "subtopics": [ + { + "slug": "distributed-microservice", + "name": "分布式微服务架构", + "description": "服务注册与发现、负载均衡、限流熔断、配置中心、幂等控制、分布式锁、分布式事务", + "path": "topics/interview-prep/distributed-microservice", + "stats": { + "total": 0, + "by_type": {} + } + }, + { + "slug": "message-queue", + "name": "消息队列", + "description": "主流中间件选型、路由、持久化原理、事务、死信队列、推拉模式、集群化部署", + "path": "topics/interview-prep/message-queue", + "stats": { + "total": 0, + "by_type": {} + } + }, + { + "slug": "k8s-observability", + "name": "K8s与可观测性", + "description": "K8s应用发布、弹性扩缩容、灰度发布、健康检查、可观测体系建设", + "path": "topics/interview-prep/k8s-observability", + "stats": { + "total": 0, + "by_type": {} + } + }, + { + "slug": "go-java-concurrency", + "name": "Go/Java并发模型", + "description": "GMP调度、CSP模型、AQS/ForkJoinPool、goroutine泄漏、JMM、happens-before、pprof/JFR", + "path": "topics/interview-prep/go-java-concurrency", + "stats": { + "total": 0, + "by_type": {} + } + }, + { + "slug": "database-advanced", + "name": "数据库进阶", + "description": "B+树索引优化、EXPLAIN执行计划、Redis集群/哨兵、混合持久化、慢查询优化", + "path": "topics/interview-prep/database-advanced", + "stats": { + "total": 0, + "by_type": {} + } + }, + { + "slug": "ai-engineering", + "name": "AI工程实践", + "description": "Context Engineer、Harness Engineer、智能体架构、MCP协议、权限治理、可插拔配置", + "path": "topics/interview-prep/ai-engineering", + "stats": { + "total": 0, + "by_type": {} + } + } + ] + }, + { + "slug": "interview-prep", + "name": "面试准备", + "description": "面试第一轮系统性复习:覆盖语言并发模型、分布式微服务、数据库进阶、消息队列、K8s与可观测性、AI工程实践", + "subtopics": [ + { + "slug": "distributed-microservice", + "name": "分布式微服务架构", + "description": "服务注册与发现、负载均衡、限流熔断、配置中心、幂等控制、分布式锁、分布式事务", + "path": "topics/interview-prep/distributed-microservice", + "stats": { + "total": 45, + "by_type": { + "single_choice": 15, + "true_false": 10, + "fill_blank": 10, + "short_answer": 5, + "code_reading": 5 + } + } + }, + { + "slug": "message-queue", + "name": "消息队列", + "description": "主流中间件选型、路由、持久化原理、事务、死信队列、推拉模式、集群化部署", + "path": "topics/interview-prep/message-queue", + "stats": { + "total": 45, + "by_type": { + "single_choice": 15, + "true_false": 10, + "fill_blank": 10, + "short_answer": 5, + "code_reading": 5 + } + } + }, + { + "slug": "k8s-observability", + "name": "K8s与可观测性", + "description": "K8s应用发布、弹性扩缩容、灰度发布、健康检查、可观测体系建设", + "path": "topics/interview-prep/k8s-observability", + "stats": { + "total": 45, + "by_type": { + "single_choice": 15, + "true_false": 10, + "fill_blank": 10, + "short_answer": 5, + "code_reading": 5 + } + } + }, + { + "slug": "go-java-concurrency", + "name": "Go/Java并发模型", + "description": "GMP调度、CSP模型、AQS/ForkJoinPool、goroutine泄漏、JMM、happens-before、pprof/JFR", + "path": "topics/interview-prep/go-java-concurrency", + "stats": { + "total": 45, + "by_type": { + "single_choice": 15, + "true_false": 10, + "fill_blank": 10, + "short_answer": 5, + "code_reading": 5 + } + } + }, + { + "slug": "database-advanced", + "name": "数据库进阶", + "description": "B+树索引优化、EXPLAIN执行计划、Redis集群/哨兵、混合持久化、慢查询优化", + "path": "topics/interview-prep/database-advanced", + "stats": { + "total": 35, + "by_type": { + "single_choice": 10, + "true_false": 5, + "fill_blank": 10, + "short_answer": 5, + "code_reading": 5 + } + } + }, + { + "slug": "ai-engineering", + "name": "AI工程实践", + "description": "Context Engineer、Harness Engineer、智能体架构、MCP协议、权限治理、可插拔配置", + "path": "topics/interview-prep/ai-engineering", + "stats": { + "total": 35, + "by_type": { + "single_choice": 10, + "true_false": 5, + "fill_blank": 10, + "short_answer": 5, + "code_reading": 5 + } + } + } + ] } ] -} +} \ No newline at end of file diff --git a/topics/interview-prep/ai-engineering/code_reading.json b/topics/interview-prep/ai-engineering/code_reading.json new file mode 100644 index 0000000..9049dd0 --- /dev/null +++ b/topics/interview-prep/ai-engineering/code_reading.json @@ -0,0 +1,315 @@ +{ + "topic": "ai-engineering", + "type": "code_reading", + "schema_version": "1.0.0", + "generated": "2026-09-09T10:00:00+08:00", + "questions": [ + { + "id": "cr-001", + "type": "code_reading", + "difficulty": 3, + "tags": [ + "tool-calling", + "ai-agent", + "harness-engineering" + ], + "question": "以下是一个 AI Agent 调用外部工具的 Python 实现。该 Agent 接收用户查询,根据 LLM 判断是否需要调用工具,并在获取工具结果后生成最终回复。请阅读代码并回答子问题。", + "code": "import json\nfrom typing import Any, Callable\n\n# 工具注册表:name -> {description, parameters, handler}\ntool_registry: dict[str, dict] = {}\n\ndef register_tool(name: str, description: str, params: dict, handler: Callable):\n \"\"\"注册一个可被 Agent 调用的工具\"\"\"\n tool_registry[name] = {\n \"description\": description,\n \"parameters\": params,\n \"handler\": handler,\n }\n\ndef call_tool(name: str, arguments: dict[str, Any]) -> str:\n \"\"\"执行工具调用,带参数校验和异常处理\"\"\"\n if name not in tool_registry:\n return json.dumps({\"error\": f\"Unknown tool: {name}\"})\n tool = tool_registry[name]\n try:\n # 按 schema 校验必填参数\n for param, spec in tool[\"parameters\"].items():\n if spec.get(\"required\") and param not in arguments:\n return json.dumps({\"error\": f\"Missing required param: {param}\"})\n result = tool[\"handler\"](**arguments)\n return json.dumps({\"result\": result})\n except Exception as e:\n return json.dumps({\"error\": str(e)})\n\ndef agent_loop(user_query: str, llm_client) -> str:\n \"\"\"Agent 主循环:查询 -> 判断 -> 调用工具 -> 拼接回复\"\"\"\n tools_schema = [\n {\"name\": k, \"description\": v[\"description\"], \"parameters\": v[\"parameters\"]}\n for k, v in tool_registry.items()\n ]\n response = llm_client.chat(user_query, tools=tools_schema)\n\n # LLM 返回 tool_calls 时执行工具\n if response.get(\"tool_calls\"):\n tool_results = []\n for tc in response[\"tool_calls\"]:\n result = call_tool(tc[\"name\"], tc[\"arguments\"])\n tool_results.append({\"tool\": tc[\"name\"], \"output\": result})\n # 将工具结果反馈给 LLM 生成最终回答\n followup = llm_client.chat(\n f\"Tools returned: {json.dumps(tool_results)}\",\n tools=tools_schema,\n )\n return followup[\"content\"]\n return response[\"content\"]", + "language": "python", + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "当 LLM 返回的 tool_calls 中包含多个工具调用时,当前代码对这些调用的执行方式是?", + "options": { + "A": "并行异步执行,使用 asyncio.gather", + "B": "串行顺序执行,逐个调用", + "C": "只执行第一个工具调用,忽略其余", + "D": "随机选择一个工具调用执行" + }, + "answer": "B", + "explanation": "代码中使用 for tc in response[\"tool_calls\"] 串行遍历并逐个调用 call_tool(),所有工具调用按顺序依次执行,没有使用异步或并发机制。" + }, + { + "index": 2, + "type": "single_choice", + "question": "call_tool 函数在遇到工具不存在时的处理策略是?", + "options": { + "A": "抛出 KeyError 异常中断流程", + "B": "返回包含 error 字段的 JSON 字符串", + "C": "自动注册该工具并返回空结果", + "D": "从 tool_registry 中删除该条目后重试" + }, + "answer": "B", + "explanation": "代码中 if name not in tool_registry 分支直接返回 json.dumps({\"error\": f\"Unknown tool: {name}\"}),即返回一个描述错误信息的 JSON 字符串,不会抛出异常。" + }, + { + "index": 3, + "type": "short_answer", + "question": "当前 agent_loop 在执行完工具调用后,将结果反馈给 LLM 生成最终回复。请指出这种设计中可能存在的一个安全风险,并简要说明如何缓解。", + "answer": "LLM 可能产生幻觉,在最终回复中编造不存在的工具调用结果", + "keywords": [ + "prompt injection", + "工具结果注入", + "结果篡改", + "二次验证", + "权限控制", + "沙箱" + ], + "scoring_rubric": "答出工具结果可能被恶意利用(如 prompt injection)或 LLM 可能忽略/篡改工具返回内容得 2 分;提出缓解措施(如对工具返回值做转义、限制 LLM 只能引用原始返回、结果二次验证等)再得 2 分;总分 4 分。", + "explanation": "工具返回的内容被直接拼入 prompt,攻击者可能通过操控工具返回来实施 prompt injection,或 LLM 可能忽略/错误引用工具结果。" + } + ], + "explanation": "这段代码展示了一个经典的 Tool Calling Agent 架构:工具注册 -> LLM 判断 -> 工具执行 -> 结果回传。核心要点包括:1) 工具通过注册表管理,支持动态扩展;2) call_tool 包含参数校验和异常兜底,保证鲁棒性;3) 串行执行工具调用在简单场景够用,但高延迟场景需要并发优化;4) 工具结果直接拼入 prompt 可能带来 prompt injection 风险。", + "source": null, + "related": [] + }, + { + "id": "cr-002", + "type": "code_reading", + "difficulty": 4, + "tags": [ + "ai-agent", + "context-engineering", + "harness-engineering" + ], + "question": "以下是一个 RAG(检索增强生成)流程的核心实现,包含向量检索、上下文拼接和最终生成。请阅读代码并回答子问题。", + "code": "from dataclasses import dataclass\nfrom typing import Optional\n\n@dataclass\nclass RetrievalResult:\n content: str\n score: float\n source: str\n\ndef retrieve(\n query: str,\n vector_store, # 向量数据库客户端\n top_k: int = 5,\n score_threshold: float = 0.7,\n) -> list[RetrievalResult]:\n \"\"\"从向量库中检索相关文档片段\"\"\"\n embeddings = vector_store.encode(query)\n raw_results = vector_store.search(embeddings, k=top_k * 2) # 过量召回\n\n # 过滤低分结果并去重\n seen = set()\n filtered = []\n for doc, score in raw_results:\n if score < score_threshold:\n continue\n dedup_key = hash(doc.content[:100])\n if dedup_key in seen:\n continue\n seen.add(dedup_key)\n filtered.append(RetrievalResult(doc.content, score, doc.source))\n return filtered[:top_k]\n\ndef build_prompt(\n query: str,\n contexts: list[RetrievalResult],\n max_context_tokens: int = 3000,\n) -> str:\n \"\"\"将检索结果拼接进 LLM prompt\"\"\"\n header = \"根据以下参考资料回答用户问题。如果资料不足请说明。\\n\\n\"\n context_parts = []\n token_budget = max_context_tokens\n\n for i, ctx in enumerate(contexts):\n chunk = f\"[参考{i+1}] (来源: {ctx.source}, 相关度: {ctx.score:.2f})\\n{ctx.content}\\n\"\n # 粗略估算 token 数(1 中文 ≈ 2 token)\n est_tokens = len(chunk) * 2\n if est_tokens > token_budget:\n break\n context_parts.append(chunk)\n token_budget -= est_tokens\n\n context_block = \"\\n\".join(context_parts)\n return f\"{header}--- 参考资料 ---\\n{context_block}\\n---\\n\\n用户问题: {query}\\n\"", + "language": "python", + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "retrieve 函数中为什么要用 top_k * 2 进行过量召回(oversampling)?", + "options": { + "A": "为了提高向量搜索的速度", + "B": "因为后续的分数过滤和去重可能淘汰部分结果,需预留余量", + "C": "为了减少向量数据库的查询次数", + "D": "为了让 LLM 有更多 token 可用" + }, + "answer": "B", + "explanation": "过量召回是为了给后续的 score_threshold 过滤和 hash 去重留出余量。如果恰好召回 top_k 条,经过过滤后可能不足 top_k 条有效结果,导致返回结果数少于预期。" + }, + { + "index": 2, + "type": "short_answer", + "question": "build_prompt 使用了 token_budget 机制来控制上下文长度。请说明这种策略的潜在问题,并提出至少一种改进方案。", + "answer": "粗略的字符数估算不够精确,可能严重高估或低估实际 token 数", + "keywords": [ + "token 计算", + "字符估算", + "tiktoken", + "tokenizer", + "动态截断", + "压缩", + "摘要" + ], + "scoring_rubric": "指出字符级估算的不准确性(如中英文混合、标点符号差异)得 2 分;提出改进方案(使用 tiktoken 精确计算、对长 chunk 做摘要压缩、按语义段落截断等)得 2 分;总分 4 分。", + "explanation": "字符级 token 估算在中英文混合场景误差大,应使用 tokenizer 库精确计算。" + }, + { + "index": 3, + "type": "single_choice", + "question": "以下关于这段 RAG 代码的说法,哪一项最准确地描述了它在生产环境中的局限?", + "options": { + "A": "代码没有调用 LLM,所以无法生成最终回答", + "B": "缺少对检索结果的相关性重排序(re-ranking),在噪声文档较多时检索质量会下降", + "C": "代码使用了同步调用,无法支持并发请求", + "D": "vector_store 的 search 方法不支持批量查询" + }, + "answer": "B", + "explanation": "这段代码仅靠向量相似度排序,没有使用 cross-encoder 或 LLM-based re-ranking 对结果进行精排。在实际生产中,向量检索的初始召回往往包含噪声,re-ranking 可以显著提升最终送入 LLM 的上下文质量。" + } + ], + "explanation": "这段 RAG 代码涵盖了三个核心阶段:1) 过量召回 + 过滤去重的检索策略,保证结果质量;2) 基于 token budget 的上下文拼接,防止超出模型窗口限制;3) 结构化 prompt 模板引导 LLM 利用参考资料。生产级 RAG 还需考虑 re-ranking、query rewriting、混合检索(向量+关键词)、上下文压缩等优化。", + "source": null, + "related": [] + }, + { + "id": "cr-003", + "type": "code_reading", + "difficulty": 3, + "tags": [ + "mcp", + "ai-agent", + "tool-calling" + ], + "question": "以下是一个 MCP(Model Context Protocol)Server 的 Python 实现,展示了如何注册和提供 Tool 给外部 Agent 调用。请阅读代码并回答子问题。", + "code": "from mcp.server import Server\nfrom mcp.types import Tool, TextContent\nimport json, os\n\nserver = Server(\"weather-server\")\n\n# 定义 MCP Tool 的元信息和参数 schema\nWEATHER_TOOL = Tool(\n name=\"get_weather\",\n description=\"获取指定城市的当前天气信息\",\n inputSchema={\n \"type\": \"object\",\n \"properties\": {\n \"city\": {\"type\": \"string\", \"description\": \"城市名称,如 Beijing\"},\n \"unit\": {\n \"type\": \"string\",\n \"enum\": [\"celsius\", \"fahrenheit\"],\n \"description\": \"温度单位\",\n \"default\": \"celsius\",\n },\n },\n \"required\": [\"city\"],\n },\n)\n\n# 注册可用工具列表\n@server.list_tools()\nasync def list_tools() -> list[Tool]:\n return [WEATHER_TOOL]\n\n# 处理工具调用请求\n@server.call_tool()\nasync def handle_call(name: str, arguments: dict) -> list[TextContent]:\n if name != \"get_weather\":\n raise ValueError(f\"Unsupported tool: {name}\")\n city = arguments[\"city\"]\n unit = arguments.get(\"unit\", \"celsius\")\n # 模拟调用外部天气 API\n api_key = os.environ.get(\"WEATHER_API_KEY\")\n if not api_key:\n return [TextContent(type=\"text\", text=json.dumps({\"error\": \"Missing API key\"}))]\n weather_data = fetch_weather_from_api(city, unit, api_key) # 外部 API 调用\n return [TextContent(type=\"text\", text=json.dumps(weather_data))]", + "language": "python", + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "MCP Server 中 inputSchema 的作用是什么?", + "options": { + "A": "定义 MCP Server 自身的配置参数", + "B": "描述 Tool 接受的输入参数结构,供调用方(如 LLM)生成符合格式的请求", + "C": "定义 Tool 返回结果的数据格式", + "D": "限制调用方只能调用该 Tool 一次" + }, + "answer": "B", + "explanation": "inputSchema 是 JSON Schema 格式的参数描述,MCP Client(通常是 LLM Agent)会读取这个 schema 来了解 tool 需要哪些参数、参数类型和是否必填,从而生成正确的调用参数。这与 OpenAI Function Calling 中的 parameters 字段作用一致。" + }, + { + "index": 2, + "type": "single_choice", + "question": "handle_call 函数返回的是 list[TextContent] 而非单个字符串,这样设计的主要原因是?", + "options": { + "A": "为了支持异步并发调用", + "B": "为了兼容 MCP 协议中一次 Tool 调用可以返回多个内容块的设计", + "C": "因为 Python 的 json.dumps 只能处理列表", + "D": "为了隐藏实际的返回数据" + }, + "answer": "B", + "explanation": "MCP 协议设计上支持 Tool 返回多种类型的内容块(文本、图片、嵌入数据等),所以返回类型是 list[Content]。当前只返回 TextContent 是最简单的情况,但如果需要返回混合内容(如图表+文本),这种设计提供了扩展性。" + }, + { + "index": 3, + "type": "short_answer", + "question": "在生产环境中,这个 MCP Server 缺少哪些关键的健壮性保障措施?请列举至少两项。", + "answer": "缺少输入参数校验、请求限流、API Key 安全管理、超时控制、调用日志记录、错误重试", + "keywords": [ + "参数校验", + "限流", + "rate limit", + "超时", + "timeout", + "日志", + "logging", + "重试", + "安全", + "认证" + ], + "scoring_rubric": "每正确列举一项健壮性措施并简要说明得 1.5 分,最高 4 分。示例:缺少参数校验(应验证 city 非空且长度合理)1.5 分;缺少限流(应防止恶意高频调用)1.5 分;缺少超时控制(外部 API 调用应有 timeout)1.5 分;缺少调用日志(应记录每次调用的入参和结果)1.5 分。", + "explanation": "生产级 MCP Server 需要完整的健壮性保障,包括参数校验、限流、超时、日志等。" + } + ], + "explanation": "这段代码展示了 MCP Server 的核心模式:1) 通过 Tool 元信息(name、description、inputSchema)声明式地注册工具能力;2) 使用装饰器模式分别处理 list_tools 和 call_tool 请求;3) 返回标准化的 Content 类型。MCP 的关键设计是将 Tool 定义与实现分离,使得 Agent 可以在运行时动态发现和调用 Tool,实现即插即用的能力扩展。", + "source": null, + "related": [] + }, + { + "id": "cr-004", + "type": "code_reading", + "difficulty": 3, + "tags": [ + "guardrail", + "ai-agent", + "llm-ops" + ], + "question": "以下是一个 LLM 输出 Guardrail 过滤器的 Python 实现,用于在将 LLM 回复发送给用户之前进行安全检查。请阅读代码并回答子问题。", + "code": "import re\nfrom dataclasses import dataclass, field\nfrom enum import Enum\n\nclass SafetyLevel(Enum):\n SAFE = \"safe\"\n WARNING = \"warning\"\n BLOCKED = \"blocked\"\n\n@dataclass\nclass GuardrailResult:\n level: SafetyLevel\n original: str\n sanitized: str = \"\"\n violations: list[str] = field(default_factory=list)\n\ndef check_pii(text: str) -> list[str]:\n \"\"\"检测文本中的个人身份信息(PII)\"\"\"\n violations = []\n # 检测手机号(中国大陆 11 位)\n if re.search(r'1[3-9]\\d{9}', text):\n violations.append(\"phone_number\")\n # 检测身份证号(18 位)\n if re.search(r'\\d{17}[\\dXx]', text):\n violations.append(\"id_card\")\n # 检测邮箱地址\n if re.search(r'[\\w.-]+@[\\w.-]+\\.\\w+', text):\n violations.append(\"email\")\n return violations\n\ndef check_prompt_leakage(text: str) -> list[str]:\n \"\"\"检测是否泄露系统提示词\"\"\"\n leak_patterns = [\n r'(?i)system\\s*prompt',\n r'(?i)you\\s+are\\s+a\\s+',\n r'(?i)ignore\\s+(previous|above|all)\\s+instructions',\n ]\n return [\"prompt_leakage\"] if any(re.search(p, text) for p in leak_patterns) else []\n\ndef apply_guardrails(output: str, strict: bool = False) -> GuardrailResult:\n \"\"\"应用所有安全检查规则,返回过滤结果\"\"\"\n violations = []\n violations.extend(check_pii(output))\n violations.extend(check_prompt_leakage(output))\n\n if not violations:\n return GuardrailResult(SafetyLevel.SAFE, output)\n # PII 敏感信息直接脱敏\n sanitized = re.sub(r'1[3-9]\\d{9}', '[手机号已脱敏]', output)\n sanitized = re.sub(r'\\d{17}[\\dXx]', '[身份证已脱敏]', sanitized)\n sanitized = re.sub(r'[\\w.-]+@[\\w.-]+\\.\\w+', '[邮箱已脱敏]', sanitized)\n # 在非严格模式下,PII 只警告不拦截;prompt leakage 始终拦截\n has_leakage = \"prompt_leakage\" in violations\n if has_leakage or strict:\n return GuardrailResult(SafetyLevel.BLOCKED, output, \"\", violations)\n return GuardrailResult(SafetyLevel.WARNING, output, sanitized, violations)", + "language": "python", + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "当 strict=False 且 LLM 输出仅包含 PII 信息(不含 prompt leakage)时,apply_guardrails 的返回结果是?", + "options": { + "A": "level=BLOCKED,sanitized 为空字符串", + "B": "level=WARNING,sanitized 包含脱敏后的文本", + "C": "level=SAFE,原样返回", + "D": "抛出异常,要求必须在 strict 模式下运行" + }, + "answer": "B", + "explanation": "代码中,当 strict=False 且没有 prompt_leakage 时,走 else 分支返回 GuardrailResult(SafetyLevel.WARNING, output, sanitized, violations),level 为 WARNING,sanitized 包含脱敏后的文本。" + }, + { + "index": 2, + "type": "short_answer", + "question": "当前的 check_pii 函数使用正则表达式检测手机号。请指出这种方法在实际生产中可能遗漏的一个 PII 类型,并简要说明为什么它难以用简单正则捕获。", + "answer": "自然语言中嵌入的地址信息难以用正则捕获", + "keywords": [ + "地址", + "姓名", + "自然语言", + "NER", + "上下文", + "语义理解", + "NLP" + ], + "scoring_rubric": "指出一种难以用正则检测的 PII 类型(如地址、姓名、银行卡号、IP 地址等)得 2 分;解释为什么难以捕获(如需要语义理解、NER 模型、上下文推理等)得 2 分;总分 4 分。", + "explanation": "地址、姓名等自然语言中的 PII 缺乏固定格式,需要 NER 模型进行语义级检测。" + }, + { + "index": 3, + "type": "single_choice", + "question": "关于这段 Guardrail 代码的设计,以下哪项评价最准确?", + "options": { + "A": "使用了 LLM-as-Judge 方案,判断精度最高", + "B": "基于规则的过滤方案,速度快但无法检测语义级别的安全问题", + "C": "完全依赖外部 API 进行安全检查,延迟最低", + "D": "使用了深度学习模型,对所有类型的内容都有效" + }, + "answer": "B", + "explanation": "该 Guardrail 使用正则表达式和关键词匹配等规则方法,优点是延迟低、可控性强、可解释性好,但缺点是无法处理语义级别的问题(如隐晦的有害内容、方言表达、上下文相关的攻击等),这些需要 NLU 模型或 LLM-based guardrail 来补充。" + } + ], + "explanation": "这段 Guardrail 代码展示了生产级 LLM 输出过滤的常见模式:1) 分层检查(PII + Prompt Leakage 分开检测);2) 脱敏而非拦截(对 PII 优先脱敏而非直接阻断,平衡安全与用户体验);3) 严格/宽松模式切换(strict 模式用于高安全场景)。实际生产中,还需补充有害内容检测(toxicity)、幻觉检测、事实性校验等模块,通常会组合使用规则引擎 + 分类模型 + LLM Judge 的多层架构。", + "source": null, + "related": [] + }, + { + "id": "cr-005", + "type": "code_reading", + "difficulty": 4, + "tags": [ + "llm-ops", + "ai-agent", + "harness-engineering" + ], + "question": "以下是一个 AI Agent 调用 LLM 时的重试与降级机制的 Python 实现。当主模型调用失败或返回异常时,系统会自动重试或切换到备用模型。请阅读代码并回答子问题。", + "code": "import time\nimport logging\nfrom dataclasses import dataclass\nfrom typing import Optional\n\nlogger = logging.getLogger(__name__)\n\n@dataclass\nclass LLMConfig:\n provider: str # 如 \"openai\", \"anthropic\", \"local\"\n model: str # 如 \"gpt-4o\", \"claude-3\"\n api_key: str\n max_retries: int = 2\n timeout: float = 30.0\n\ndef call_llm_with_retry(\n prompt: str,\n configs: list[LLMConfig], # 按优先级排列的模型配置列表\n fallback_response: str = \"抱歉,服务暂时不可用,请稍后重试。\",\n) -> str:\n \"\"\"带重试和降级的 LLM 调用\"\"\"\n last_error = None\n\n for config in configs:\n for attempt in range(config.max_retries + 1):\n try:\n logger.info(f\"Calling {config.provider}/{config.model} (attempt {attempt + 1})\")\n response = _call_provider(config, prompt)\n\n # 检查响应质量(空内容或错误标记)\n if not response or response.strip() == \"\":\n raise ValueError(\"Empty response from LLM\")\n if response.startswith(\"[ERROR]\"):\n raise ValueError(f\"LLM returned error: {response}\")\n\n logger.info(f\"Success with {config.provider}/{config.model}\")\n return response\n\n except Exception as e:\n last_error = e\n logger.warning(\n f\"{config.provider}/{config.model} failed \"\n f\"(attempt {attempt + 1}): {e}\"\n )\n # 指数退避:等待 1s, 2s, 4s ...\n if attempt < config.max_retries:\n wait = 2 ** attempt\n logger.info(f\"Retrying in {wait}s...\")\n time.sleep(wait)\n\n # 当前 provider 的所有重试耗尽,切换到下一个\n logger.warning(f\"All retries exhausted for {config.provider}, falling back...\")\n\n # 所有 provider 都失败,返回兜底响应\n logger.error(f\"All LLM providers failed. Last error: {last_error}\")\n return fallback_response\n\ndef _call_provider(config: LLMConfig, prompt: str) -> str:\n \"\"\"实际调用 LLM provider(此处为占位实现)\"\"\"\n if config.provider == \"openai\":\n # 实际会调用 OpenAI API\n return _call_openai(config.model, prompt, config.api_key, config.timeout)\n elif config.provider == \"anthropic\":\n return _call_anthropic(config.model, prompt, config.api_key, config.timeout)\n else:\n raise ValueError(f\"Unknown provider: {config.provider}\")", + "language": "python", + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "代码中使用了 2 ** attempt 作为重试间隔。当某个 provider 的 max_retries=3 时,前四次尝试(含首次)的等待时间序列是?", + "options": { + "A": "0s, 1s, 2s, 4s", + "B": "1s, 2s, 4s, 8s", + "C": "1s, 1s, 1s, 1s", + "D": "0s, 0s, 0s, 0s" + }, + "answer": "A", + "explanation": "attempt 从 0 开始:第 1 次 attempt=0(首次调用,不等待),第 2 次 attempt=0 失败后 wait=2^0=1s,第 3 次 attempt=1 失败后 wait=2^1=2s,第 4 次 attempt=2 失败后 wait=2^2=4s。所以等待序列为 0s, 1s, 2s, 4s。" + }, + { + "index": 2, + "type": "single_choice", + "question": "当 configs 列表中有 2 个 provider 且都调用失败时,函数最终返回什么?", + "options": { + "A": "抛出异常,不返回任何值", + "B": "返回最后一个 provider 最后一次调用的原始异常信息", + "C": "返回 fallback_response 参数指定的兜底文本", + "D": "返回空字符串" + }, + "answer": "C", + "explanation": "两个 for 循环(provider 循环 + attempt 循环)都执行完后,函数执行最后的 return fallback_response,返回调用方传入的兜底响应文本。这是防御性编程的典型模式——永远给用户一个有意义的回复。" + }, + { + "index": 3, + "type": "short_answer", + "question": "当前实现使用了 time.sleep() 进行同步阻塞等待。如果这个函数在异步 Web 服务中被调用,会有什么问题?请提出一种不阻塞事件循环的替代方案。", + "answer": "time.sleep 会阻塞整个事件循环,导致同进程内其他并发请求无法处理", + "keywords": [ + "阻塞", + "事件循环", + "asyncio", + "异步", + "await", + "asyncio.sleep", + "线程池" + ], + "scoring_rubric": "正确指出 time.sleep 会阻塞事件循环导致并发能力丧失得 2 分;提出 asyncio.sleep 或将重试逻辑放入线程池(run_in_executor)等非阻塞方案得 2 分;总分 4 分。", + "explanation": "同步阻塞等待在异步框架中是致命的,会严重影响服务并发能力,应使用 asyncio.sleep 替代。" + } + ], + "explanation": "这段代码展示了生产级 LLM 调用的重试与降级策略:1) 多级 fallback:按配置优先级依次尝试不同 provider;2) 指数退避:避免失败时立即重试造成 provider 过载;3) 响应质量检查:不仅捕获异常,还检查空响应和错误标记;4) 兜底响应:所有渠道失败时仍给用户一个有意义的回复。实际生产中还需考虑 circuit breaker(熔断器)、成功率监控、不同 provider 的 token 计费差异、以及异步化改造以适配高并发场景。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/interview-prep/ai-engineering/fill_blank.json b/topics/interview-prep/ai-engineering/fill_blank.json new file mode 100644 index 0000000..fbe7a51 --- /dev/null +++ b/topics/interview-prep/ai-engineering/fill_blank.json @@ -0,0 +1,217 @@ +{ + "topic": "ai-engineering", + "type": "fill_blank", + "schema_version": "1.0.0", + "generated": "2026-09-09T10:00:00+08:00", + "questions": [ + { + "id": "fb-001", + "type": "fill_blank", + "difficulty": 2, + "tags": [ + "context-engineering" + ], + "question": "在 RAG(Retrieval-Augmented Generation)系统中,向量数据库的核心作用是将文本块(chunk)通过 Embedding 模型转化为高维向量后进行存储,并在查询时通过______匹配返回与用户问题语义最相近的文档片段,从而为 LLM 提供上下文信息。", + "answer": [ + "相似度", + "向量相似度", + "语义相似度", + "余弦相似度", + "近似" + ], + "answer_rule": "any", + "explanation": "RAG 系统的工作流程分为检索(Retrieval)和生成(Generation)两个阶段。在检索阶段,用户的问题先通过 Embedding 模型编码为向量,然后在向量数据库中执行相似度搜索(如余弦相似度、欧氏距离、内积等),找到与问题语义最接近的 top-k 文档片段。向量数据库(如 Milvus、Pinecone、Chroma、FAISS)的核心价值在于高效地进行近似最近邻(ANN)搜索,避免了传统关键词匹配无法处理语义理解的局限性。检索到的文档片段作为上下文注入 Prompt,帮助 LLM 生成更准确、有依据的回答。", + "source": null, + "related": [] + }, + { + "id": "fb-002", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "context-engineering" + ], + "question": "在 Prompt 模板设计中,系统角色定义(System Role)通常放在对话的最前面,用于约束 LLM 的行为边界和输出格式。这种将角色、指令和约束统一注入 Prompt 的工程方法被称为______,它强调通过系统性地构造上下文来控制 LLM 的行为。", + "answer": [ + "Prompt Engineering", + "prompt engineering", + "提示工程", + "提示词工程" + ], + "answer_rule": "any", + "explanation": "Prompt Engineering(提示工程)是指通过精心设计输入给 LLM 的文本(Prompt),来引导模型产生期望输出的工程方法。System Role 是 Prompt Engineering 中的关键组成部分,它定义了模型的身份、能力边界、行为准则和输出格式。例如,在一个客服助手中的 System Role 可能定义为「你是一个专业的客服助手,只能回答与产品相关的问题,回复时使用简洁的语气」。好的 System Role 设计能显著降低模型幻觉率、提高输出一致性和可控性。在更广泛的 Context Engineering(上下文工程)视角下,System Role 只是上下文构建的一个环节,还包括检索注入、Few-shot 示例、工具描述等多个维度。", + "source": null, + "related": [] + }, + { + "id": "fb-003", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "harness-engineering", + "ai-agent" + ], + "question": "ReAct(Reasoning + Acting)模式是一种将 LLM 的推理和行动能力结合的智能体框架。其核心循环包含三个步骤:首先 LLM 进行内部推理(Thought),然后决定并执行一个外部操作(Action),最后根据操作结果获取反馈(______),并将反馈作为下一轮推理的输入。", + "answer": [ + "Observation", + "observation", + "观察" + ], + "answer_rule": "any", + "explanation": "ReAct 模式由 Yao et al. (2022) 在论文《ReAct: Synergizing Reasoning and Acting in Language Models》中提出。其核心思想是让 LLM 交替进行推理(Thought)和行动(Action),形成 Thought → Action → Observation 的循环。具体流程为:(1) Thought:LLM 分析当前状态,制定下一步计划;(2) Action:LLM 选择并调用一个工具(如搜索引擎、计算器、API);(3) Observation:将工具返回的结果作为观察反馈给 LLM。这个循环不断重复,直到 LLM 认为已获得足够信息可以给出最终答案。ReAct 的优势在于将 Chain-of-Thought(思维链)推理与工具调用无缝结合,既保持了推理的可解释性,又扩展了模型的能力边界。LangChain、LlamaIndex 等主流框架都实现了 ReAct Agent。", + "source": null, + "related": [] + }, + { + "id": "fb-004", + "type": "fill_blank", + "difficulty": 4, + "tags": [ + "tool-calling", + "harness-engineering" + ], + "question": "在 LLM 的 Tool Calling(函数调用)机制中,当模型决定使用某个工具时,它会输出一个符合 JSON Schema 规范的结构化参数。这个参数对象中,最外层必须包含 \"name\"(工具名称)和 \"______\"(工具所需的输入参数)两个核心字段。", + "answer": [ + "arguments", + "parameters", + "input", + "params", + "args" + ], + "answer_rule": "any", + "explanation": "现代 LLM 的 Tool Calling 机制(如 OpenAI 的 Function Calling、Anthropic 的 Tool Use)要求模型以结构化 JSON 格式输出工具调用请求。标准格式包含两个核心字段:(1) name:要调用的工具/函数名称,必须与系统中注册的工具名精确匹配;(2) arguments(或 parameters):一个 JSON 对象,包含该工具所需的全部输入参数,参数结构需符合预先定义的 JSON Schema。例如,调用天气查询工具时,输出可能为 {\"name\": \"get_weather\", \"arguments\": {\"city\": \"北京\", \"unit\": \"celsius\"}}。框架层负责解析这个 JSON 并执行实际的函数调用,然后将结果返回给 LLM 继续推理。这种设计将 LLM 的「意图理解」与「工具执行」解耦,是 Harness Engineering(驾驭工程)的核心模式之一。", + "source": null, + "related": [] + }, + { + "id": "fb-005", + "type": "fill_blank", + "difficulty": 4, + "tags": [ + "ai-agent" + ], + "question": "在多智能体架构中,Agent 之间协作的一种常见模式是「共享状态」模式:多个 Agent 共同访问和修改一个中心化的______,每个 Agent 读取共享信息、执行自己的子任务后,将结果写回该状态,从而实现跨 Agent 的信息流转与协作。", + "answer": [ + "状态", + "上下文", + "shared state", + "上下文状态", + "状态空间", + "黑板", + "黑板系统", + "共享上下文", + "全局状态" + ], + "answer_rule": "any", + "explanation": "在多智能体系统(Multi-Agent System)中,Agent 间的协作模式主要有三种:(1) 共享状态(Shared State/Blackboard)模式:所有 Agent 共享一个中心化的状态存储(如 Blackboard),每个 Agent 可以读取全局状态、执行子任务并将结果写回,实现松耦合协作。LangGraph 的 StateGraph 就是这种模式的典型实现。(2) 消息传递(Message Passing)模式:Agent 之间通过直接发送消息通信,没有共享状态。(3) 层级委托(Hierarchical Delegation)模式:由一个主 Agent 将任务分解并委派给子 Agent。共享状态模式的优势是实现简单、状态透明,但需要注意并发写入时的竞态条件和状态一致性问题。在实际工程中,常结合状态快照(Checkpoint)和事务机制来保证可靠性。", + "source": null, + "related": [] + }, + { + "id": "fb-006", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "mcp", + "ai-agent" + ], + "question": "MCP(Model Context Protocol)是由 Anthropic 提出的开放协议,用于标准化 LLM 应用与外部数据源和工具之间的连接。MCP 的三大核心要素是:Server(提供能力的服务端)、Client(消费能力的客户端),以及定义在 Server 和 Client 之间通信的______。", + "answer": [ + "协议", + "通信协议", + "消息协议", + "Protocol", + "transport", + "传输层", + "JSON-RPC" + ], + "answer_rule": "any", + "explanation": "MCP 的架构围绕三个核心要素展开:(1) MCP Server:暴露工具(Tools)、资源(Resources)和提示模板(Prompts)的服务端程序,负责与具体的外部数据源或 API 对接。(2) MCP Client:嵌入在 LLM 应用(如 IDE、聊天界面)中的客户端,负责发现和调用 Server 提供的能力。(3) 协议本身:定义了 Server 和 Client 之间基于 JSON-RPC 2.0 的通信规范,包括能力发现(initialize)、工具调用(tools/call)、资源读取(resources/read)等标准方法。MCP 的核心价值在于解决了「M×N 集成问题」——传统方式下 M 个 LLM 应用对接 N 个工具需要 M×N 个适配器,而 MCP 将其降低为 M+N 个。协议支持 stdio 和 HTTP+SSE 两种传输方式。", + "source": null, + "related": [] + }, + { + "id": "fb-007", + "type": "fill_blank", + "difficulty": 4, + "tags": [ + "permission" + ], + "question": "在 AI Agent 的权限治理中,沙箱(Sandbox)执行环境通过进程隔离、文件系统隔离和______隔离等机制,确保 Agent 执行的代码或命令不会影响宿主系统安全。常见的沙箱技术包括 Docker 容器、gVisor 和 Firecracker。", + "answer": [ + "网络", + "网络隔离", + "namespace", + "命名空间", + "资源", + "资源隔离", + "cgroup" + ], + "answer_rule": "any", + "explanation": "沙箱执行环境是 AI Agent 权限治理的核心防线,通过多层隔离机制限制 Agent 的能力边界:(1) 进程隔离:使用 Linux namespace 或容器技术,使 Agent 进程无法看到或访问宿主系统的其他进程。(2) 文件系统隔离:通过只读挂载或 overlay filesystem 限制 Agent 只能访问特定目录,阻止读取/修改敏感文件。(3) 网络隔离:通过网络命名空间、iptables 规则或代理层限制 Agent 的网络访问范围,防止未授权的外部请求。(4) 资源隔离:使用 cgroup 限制 CPU、内存、磁盘 I/O 等资源使用,防止资源耗尽攻击。在 QwenPaw 等框架中,sandbox_config 支持配置 mount 权限和网络限制,确保 Agent 只能在授权范围内执行操作。gVisor 和 Firecracker 提供了比传统 Docker 更强的内核级隔离。", + "source": null, + "related": [] + }, + { + "id": "fb-008", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "pluggable-config" + ], + "question": "在 AI 框架的可插拔配置设计中,Provider 抽象层通过统一的接口(如 chat、embed、complete 等方法)屏蔽不同 LLM 供应商的 API 差异。当需要切换底层模型(如从 OpenAI 切换到 Qwen)时,只需修改配置中的______字段,无需改动业务代码。", + "answer": [ + "provider", + "provider_id", + "providerId", + "provider_name", + "模型供应商" + ], + "answer_rule": "any", + "explanation": "Provider 抽象层是现代 AI 框架实现可插拔架构的关键设计模式。其核心思想是定义一套标准化接口(通常包括 chat/completion、embedding、function calling 等方法),然后为不同的 LLM 供应商(OpenAI、Anthropic、Qwen、本地模型等)实现具体的适配器。配置层面通常通过 provider 字段(或 provider_id)来指定使用哪个后端。例如在 QwenPaw 中,切换模型只需在配置文件中修改 provider 和 model 字段,框架会自动路由到对应的适配器实现。这种设计遵循了 SOLID 原则中的依赖倒置原则(DIP)——业务代码依赖于抽象接口而非具体实现。Provider 抽象层还统一处理了认证管理、请求重试、响应格式标准化、Token 计费等横切关注点,极大降低了切换和维护成本。", + "source": null, + "related": [] + }, + { + "id": "fb-009", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "guardrail" + ], + "question": "在 LLM 应用的 Guardrail(安全护栏)系统中,PII(Personally Identifiable Information,个人身份信息)检测是重要的防护环节。通过预定义的______表达式(如匹配手机号、身份证号、邮箱地址等模式),可以在输入和输出阶段自动识别并脱敏敏感信息,防止数据泄露。", + "answer": [ + "正则", + "正则表达式", + "regex", + "Regular Expression", + "regexp" + ], + "answer_rule": "any", + "explanation": "PII 检测是 Guardrail 系统的基础能力之一。正则表达式(Regular Expression)因其高效、确定性强的特点,是实现 PII 模式匹配的首选技术。常见的正则匹配策略包括:(1) 中国大陆手机号:以 1 开头、第二位 3-9、后续 9 位数字;(2) 身份证号:18 位,包含地区码、出生日期、顺序码和校验位;(3) 邮箱地址:用户名 + @ + 域名格式。在实际 Guardrail 架构中,正则匹配通常作为第一层快速筛查(低延迟、高召回),配合 NER(命名实体识别)模型作为第二层精细检测(高精度、低误报)。检测到 PII 后的处理策略包括:阻断(Block)、脱敏(Mask/Redact,如将身份证号替换为占位符)、或告警。Guardrail 系统还应支持正则规则的热更新,以便快速应对新出现的 PII 模式。", + "source": null, + "related": [] + }, + { + "id": "fb-010", + "type": "fill_blank", + "difficulty": 4, + "tags": [ + "llm-ops" + ], + "question": "在 LLM 应用的可靠性工程中,当 API 调用失败(如限流 429 错误)时,指数退避重试是标准策略。其等待时间的计算公式为:delay = min(base * 2^n + random, max_delay),其中 base 是基础延迟,n 是当前重试次数,max_delay 是最大等待上限,random 是随机抖动值。增加随机抖动(jitter)的目的是防止多个客户端在重试时产生______效应。", + "answer": [ + "惊群", + "惊群效应", + "thundering herd", + "Thundering Herd", + "同步", + "同步重试" + ], + "answer_rule": "any", + "explanation": "指数退避(Exponential Backoff)是分布式系统中最常用的重试策略,公式为 delay = min(base * 2^n + jitter, max_delay)。其中 base 通常设为 1 秒,n 从 0 开始递增。例如第 1 次重试等待约 1 秒,第 2 次约 2 秒,第 3 次约 4 秒,依此类推直到 max_delay(通常 60-120 秒)。引入随机抖动(jitter)至关重要——如果不加抖动,所有遇到 429 错误的客户端会在完全相同的时间点重试,形成「惊群效应」(Thundering Herd),导致服务端在短时间内再次承受峰值压力,陷入反复限流的恶性循环。常见的 jitter 策略有:(1) Full Jitter:在 [0, base*2^n] 范围内随机取值;(2) Equal Jitter:基础值的一半 + 随机值;(3) Decorrelated Jitter:基于上一次等待时间计算。在 LLM Ops 实践中,建议结合 Circuit Breaker(熔断器)使用,当连续失败超过阈值时暂停重试,避免无意义的资源消耗。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/interview-prep/ai-engineering/meta.json b/topics/interview-prep/ai-engineering/meta.json new file mode 100644 index 0000000..bb06793 --- /dev/null +++ b/topics/interview-prep/ai-engineering/meta.json @@ -0,0 +1,25 @@ +{ + "slug": "ai-engineering", + "name": "AI工程实践", + "description": "Context Engineer、Harness Engineer、智能体架构、MCP协议、权限治理、可插拔配置", + "tags": [], + "question_files": { + "single_choice": "single_choice.json", + "true_false": "true_false.json", + "fill_blank": "fill_blank.json", + "short_answer": "short_answer.json", + "code_reading": "code_reading.json" + }, + "stats": { + "total": 35, + "by_type": { + "single_choice": 10, + "true_false": 5, + "fill_blank": 10, + "short_answer": 5, + "code_reading": 5 + } + }, + "created": "2026-09-09", + "updated": "2026-09-09" +} \ No newline at end of file diff --git a/topics/interview-prep/ai-engineering/short_answer.json b/topics/interview-prep/ai-engineering/short_answer.json new file mode 100644 index 0000000..7ffc49b --- /dev/null +++ b/topics/interview-prep/ai-engineering/short_answer.json @@ -0,0 +1,148 @@ +{ + "topic": "ai-engineering", + "type": "short_answer", + "schema_version": "1.0.0", + "generated": "2026-09-09T10:00:00+08:00", + "questions": [ + { + "id": "sa-001", + "type": "short_answer", + "difficulty": 3, + "tags": [ + "context-engineering", + "llm-ops" + ], + "question": "Context Engineer 在 AI 工程体系中的核心职责是什么?请从信息组织、上下文窗口管理、动态检索策略三个维度阐述其工作范围,并说明它与传统 Prompt Engineering 的关键区别。", + "answer": "Context Engineer 的核心职责是设计和管理 LLM 的输入上下文,确保模型在推理时获得最相关、最精炼的信息。具体包括三个维度:(1) 信息组织:构建结构化的上下文体系,包括系统提示、用户对话历史、检索文档、工具调用结果等多源信息的编排与优先级排序;(2) 上下文窗口管理:在有限的 token 预算内进行信息压缩、截断、摘要,处理长上下文场景下的注意力衰减问题,必要时实施分块策略;(3) 动态检索策略:根据用户意图实时决定何时检索、检索什么、检索多少,实现 RAG 管线的精准触发与结果融合。与传统 Prompt Engineering 的关键区别在于:Prompt Engineering 关注单次调用的提示词措辞优化,而 Context Engineer 关注的是一个持续的、系统性的上下文生命周期管理——涵盖信息的采集、筛选、组织、缓存、刷新和评估,是一种工程化而非手艺化的思维方式。", + "keywords": [ + "信息组织", + "上下文窗口", + "token 预算", + "动态检索", + "RAG", + "Prompt Engineering 区别", + "上下文生命周期", + "信息压缩", + "分块策略", + "优先级排序" + ], + "scoring_rubric": "满分10分。信息组织维度(2分):需提及多源信息编排和优先级排序。上下文窗口管理维度(2分):需提及 token 预算、压缩/摘要、注意力衰减等概念。动态检索策略维度(2分):需提及 RAG 管线和实时触发。与 Prompt Engineering 的区别(2分):需明确对比单次措辞优化 vs 系统性生命周期管理。整体表达(2分):逻辑清晰、术语准确。", + "explanation": "Context Engineering 是 2024-2025 年 AI 工程领域兴起的核心概念,由 Andrej Karpathy 等人推广。它标志着 AI 应用开发从「调提示词」到「管上下文」的范式转变。传统 Prompt Engineering 像是编辑一篇文章的措辞,而 Context Engineer 则像是设计整个信息管道——决定什么信息在什么时候以什么形式进入模型的视野。这一角色在 RAG 系统、多轮对话、Agent 系统中尤为重要,因为这些场景下上下文的质量直接决定了输出的质量。", + "source": null, + "related": [] + }, + { + "id": "sa-002", + "type": "short_answer", + "difficulty": 4, + "tags": [ + "ai-agent", + "harness-engineering", + "tool-calling" + ], + "question": "请对比 ReAct 与 Plan-and-Execute 两种 Agent 执行模式的核心差异,分别说明它们的执行流程、适用场景和各自的优缺点,并给出在实际项目中选择这两种模式的决策依据。", + "answer": "ReAct(Reasoning + Acting)模式采用「思考-行动-观察」的交替循环:每一步先推理下一步该做什么,执行工具调用,获取结果后再推理下一步。其执行流程是线性的、即时决策的。优点是实现简单、对简单任务响应快、每步都有充分的环境反馈;缺点是缺乏全局规划,在多步骤复杂任务中容易偏离目标、陷入循环或重复无效操作。Plan-and-Execute 模式分为两阶段:先由 Planner 生成完整的执行计划(任务分解),再由 Executor 按计划逐步执行,执行过程中可反馈修正计划。优点是对复杂任务有全局视野、可预估资源消耗、支持并行执行子任务;缺点是计划生成本身消耗额外 token、初始计划可能不准确需要修正机制、对动态变化的环境适应性较弱。选择决策依据:(1) 任务步骤少于 3-4 步且逻辑简单时选 ReAct;(2) 任务可清晰分解、有明确子目标时选 Plan-and-Execute;(3) 需要实时适应环境变化的场景选 ReAct;(4) 对执行成本和时间有预算约束时选 Plan-and-Execute(可提前预估)。", + "keywords": [ + "ReAct", + "Plan-and-Execute", + "思考-行动-观察", + "任务分解", + "执行计划", + "全局规划", + "循环执行", + "并行执行", + "token 消耗", + "决策依据" + ], + "scoring_rubric": "满分10分。ReAct 模式描述(2分):需提及交替循环和即时决策特征。Plan-and-Execute 模式描述(2分):需提及两阶段分离和计划修正机制。优缺点对比(3分):双方各至少一个优点和一个缺点,共需覆盖至少4个对比维度。选择决策依据(2分):需给出至少2条具体的决策条件。整体表达(1分):逻辑清晰、术语准确。", + "explanation": "ReAct 来自 Yao et al. 2022 年的论文,将 CoT 推理与工具调用交替进行,是当前大多数 Agent 框架(如 LangChain AgentExecutor)的默认模式。Plan-and-Execute 则参考了人类「先想后做」的策略,LangGraph、AutoGPT 等框架支持这种模式。在实际工程中,很多成熟的 Agent 系统会混合使用:对顶层任务使用 Plan-and-Execute 做全局规划,对子任务内部使用 ReAct 做灵活执行。选择时还需考虑 LLM 的能力——较弱的模型可能无法生成可靠的全局计划,此时 ReAct 的逐步决策反而更稳健。", + "source": null, + "related": [] + }, + { + "id": "sa-003", + "type": "short_answer", + "difficulty": 3, + "tags": [ + "ai-agent", + "pluggable-config" + ], + "question": "在智能体架构设计中,单 Agent 与多 Agent 架构分别适用于什么场景?请从任务复杂度、系统可靠性、开发维护成本、扩展性四个方面进行对比分析,并举出一个适合采用多 Agent 架构的典型业务场景。", + "answer": "单 Agent 架构适用于:任务逻辑清晰、工具调用链路短、领域单一的场景。多 Agent 架构适用于:任务涉及多个专业领域、需要角色分工协作、工作流复杂的场景。四维度对比:(1) 任务复杂度:单 Agent 适合步骤少于 5-7 步的线性任务;多 Agent 适合需要并行处理、条件分支、多轮协商的复杂工作流。(2) 系统可靠性:单 Agent 故障点集中,一个环节出错影响整体;多 Agent 通过职责隔离降低级联故障风险,但引入了 Agent 间通信的故障点。(3) 开发维护成本:单 Agent 开发周期短、调试简单;多 Agent 需要设计通信协议、状态同步、错误恢复机制,维护成本显著更高。(4) 扩展性:单 Agent 功能扩展受上下文窗口限制,新工具增加会稀释已有能力;多 Agent 可通过新增专业 Agent 实现水平扩展,各 Agent 独立迭代。典型多 Agent 场景:企业级代码审查系统——由代码分析 Agent 负责静态检查、安全审计 Agent 负责漏洞扫描、架构评审 Agent 负责设计模式评估、报告生成 Agent 汇总结果,各 Agent 并行工作后由编排器合并输出。", + "keywords": [ + "单 Agent", + "多 Agent", + "职责隔离", + "角色分工", + "通信协议", + "上下文窗口", + "水平扩展", + "编排器", + "故障点", + "级联故障" + ], + "scoring_rubric": "满分10分。单 Agent 适用场景(1分)。多 Agent 适用场景(1分)。四维度对比(各1.5分,共6分):需覆盖任务复杂度、可靠性、成本、扩展性。典型场景举例(2分):需具体且合理。整体表达(1分)。", + "explanation": "架构选择的核心原则是「够用就好」——单 Agent 能解决的问题不要过度设计为多 Agent。多 Agent 的优势在于专业化分工(每个 Agent 的 prompt 更聚焦、工具集更精简),但代价是协调开销。在实际工程中,很多团队采用「混合架构」:核心流程用单 Agent,特定子流程拆分为独立 Agent。编排方式也有多种选择——中心化编排器(Orchestrator)、去中心化协商(Chat/Debate)、流水线(Pipeline)等,需要根据业务特点选择。", + "source": null, + "related": [] + }, + { + "id": "sa-004", + "type": "short_answer", + "difficulty": 4, + "tags": [ + "mcp", + "tool-calling", + "permission" + ], + "question": "MCP(Model Context Protocol)协议在设计时需要考虑哪些安全问题?请从身份认证、权限控制、数据隔离、传输安全、防注入攻击五个方面阐述 MCP 的安全考量与设计原则,并说明 MCP Host、MCP Server、MCP Client 三者在安全责任上的分工。", + "answer": "MCP 协议的安全考量涵盖五个核心方面:(1) 身份认证:MCP Client 与 MCP Server 之间需要建立双向身份验证,通常基于 OAuth 2.0 或 API Key,确保只有授权的 Client 能访问 Server 暴露的工具和资源。(2) 权限控制:MCP Server 应遵循最小权限原则,每个工具声明所需的权限级别,Host 层面实施用户确认机制(human-in-the-loop),对敏感操作(如文件写入、数据库修改)要求用户显式授权。(3) 数据隔离:不同 Client 的会话数据应严格隔离,Server 不得在未授权情况下跨会话共享数据;Server 本地存储的凭证和配置文件需要加密保护。(4) 传输安全:所有 MCP 通信应使用 TLS/HTTPS 加密传输,本地 stdio 传输模式需确保进程间通信不被第三方窃听。(5) 防注入攻击:MCP Server 需要对 LLM 生成的工具调用参数进行严格校验和转义,防止 Prompt Injection 通过工具参数传递到下游系统;Server 不应盲目执行未经验证的指令。安全责任分工:MCP Host(如 IDE、聊天应用)负责用户身份管理、权限策略制定和 human-in-the-loop 审批;MCP Client(协议客户端)负责建立安全连接、传输层加密和会话隔离;MCP Server(工具提供方)负责输入校验、权限边界执行和数据保护。", + "keywords": [ + "MCP", + "身份认证", + "OAuth 2.0", + "最小权限", + "human-in-the-loop", + "数据隔离", + "TLS", + "Prompt Injection", + "输入校验", + "安全责任分工" + ], + "scoring_rubric": "满分10分。五个安全方面(各1.2分,共6分):每方面需给出具体的安全措施或设计原则。安全责任分工(2分):需清晰区分 Host、Client、Server 的职责。整体表达(2分):术语准确、逻辑清晰。", + "explanation": "MCP 协议由 Anthropic 于 2024 年底提出并开源,旨在为 LLM 应用提供标准化的工具和资源访问协议。安全是 MCP 生态的核心关注点——因为 MCP Server 本质上是让 LLM 代理用户执行操作,一旦被恶意利用(如通过 Prompt Injection 诱导 LLM 调用危险工具),后果可能很严重。MCP 规范中推荐的 OAuth 2.0 PKCE 流程、工具的 annotation 标注(如 readOnlyHint、destructiveHint)以及 Host 层的用户确认机制,都是为了在便利性和安全性之间取得平衡。在企业场景中,还需要额外考虑审计日志、操作回滚、沙箱隔离等措施。", + "source": null, + "related": [] + }, + { + "id": "sa-005", + "type": "short_answer", + "difficulty": 4, + "tags": [ + "guardrail", + "llm-ops", + "tool-calling", + "permission" + ], + "question": "请描述一个生产级 LLM 应用中 Guardrail(护栏)系统的完整实现方案,包括输入侧护栏和输出侧护栏各自承担的职责、具体的技术实现手段,以及 Guardrail 系统在整体架构中的位置和与主 LLM 调用链路的关系。", + "answer": "生产级 Guardrail 系统分为输入侧和输出侧两道防线,包裹在主 LLM 调用链路的前后。输入侧护栏职责:在用户输入到达 LLM 之前进行安全过滤和预处理。具体技术手段包括:(1) Prompt Injection 检测——使用分类器或规则引擎识别恶意指令注入尝试;(2) 敏感信息脱敏——正则匹配或 NER 模型识别并替换手机号、身份证号等 PII 数据;(3) 输入长度和格式校验——防止超长输入导致 token 溢出;(4) 意图分类和话题限制——识别超出系统能力范围或不在授权领域内的请求。输出侧护栏职责:在 LLM 响应返回用户之前进行质量和安全审核。具体技术手段包括:(1) 幻觉检测——将输出与检索到的源文档交叉验证,检测无据可依的事实陈述;(2) 有害内容过滤——使用毒性分类模型检测仇恨、暴力、违法内容;(3) 格式合规检查——验证 JSON Schema、工具调用格式等结构化输出是否合规;(4) 敏感信息防泄露——检测模型输出中是否包含训练数据泄露或内部系统信息;(5) 事实一致性校验——确保多轮对话中的输出不与已确认的事实矛盾。架构位置与关系:Guardrail 系统作为独立的中间件层,以管道(Pipeline)模式嵌入主调用链路——输入护栏在用户请求之后、LLM 调用之前执行,输出护栏在 LLM 返回之后、用户接收之前执行。两者都采用异步可选的执行模式:拦截到高风险问题时直接拒绝并返回安全提示,低风险问题可标记后放行。在高吞吐场景下,Guardrail 可使用轻量级模型(如 BERT-based 分类器)以控制延迟。", + "keywords": [ + "Guardrail", + "输入护栏", + "输出护栏", + "Prompt Injection 检测", + "PII 脱敏", + "幻觉检测", + "有害内容过滤", + "格式合规", + "管道模式", + "分类器", + "human-in-the-loop" + ], + "scoring_rubric": "满分10分。输入侧护栏(3分):需涵盖至少3种具体技术手段。输出侧护栏(3分):需涵盖至少3种具体技术手段。架构位置与关系(2分):需说明管道模式和独立中间件层。整体表达(2分):结构清晰、技术细节准确。", + "explanation": "Guardrail 是 LLM 应用从实验走向生产的必备组件。业界常用的实现方案包括:NVIDIA NeMo Guardrails(开源,支持对话流控制和主题限制)、Guardrails AI(专注输出校验和自动修复)、Lakera Guard(专注 Prompt Injection 检测)。在实际工程中,Guardrail 的设计需要权衡安全性和延迟——过严的过滤会降低用户体验,过松则可能放行有害内容。最佳实践是分层防御(Defense in Depth):轻量级规则引擎做快速拦截,重量级模型做精细判断,结合人工审核处理边界案例。此外,Guardrail 系统本身也需要监控和迭代——通过 A/B 测试和人工标注持续优化检测模型的准确率和召回率。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/interview-prep/ai-engineering/single_choice.json b/topics/interview-prep/ai-engineering/single_choice.json new file mode 100644 index 0000000..8bb0852 --- /dev/null +++ b/topics/interview-prep/ai-engineering/single_choice.json @@ -0,0 +1,200 @@ +{ + "topic": "ai-engineering", + "type": "single_choice", + "schema_version": "1.0.0", + "generated": "2026-09-09T10:00:00+08:00", + "questions": [ + { + "id": "sc-001", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "context-engineering" + ], + "question": "在 RAG(Retrieval-Augmented Generation)系统中,以下哪种检索策略最能有效提升最终生成答案的相关性?", + "options": { + "A": "始终返回 Top-1 相关性最高的文档片段", + "B": "使用语义向量检索 + 重排序(Re-ranking)的两阶段检索", + "C": "将所有检索到的文档一次性拼接后发送给 LLM", + "D": "仅依赖关键词 BM25 检索,不做向量化处理" + }, + "answer": "B", + "explanation": "两阶段检索(先粗召回、再精排)是当前 RAG 系统的最佳实践。第一阶段用向量检索从大量文档中快速召回候选集,第二阶段用 Cross-Encoder 等重排序模型对候选集精排,能显著提升检索到的文档与查询的相关性。A 选项只返回一个片段,信息量不足且容错性差;C 选项将所有文档拼接会导致上下文窗口浪费和信息噪声;D 选项仅依赖关键词检索无法处理语义相近但词汇不同的查询,召回率不足。", + "source": null, + "related": [] + }, + { + "id": "sc-002", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "context-engineering" + ], + "question": "当 LLM 的上下文窗口有限时,以下哪种策略最适合处理一篇超过窗口大小的长文档摘要任务?", + "options": { + "A": "将文档截断至上下文窗口大小,直接丢弃超出部分", + "B": "采用 Map-Reduce 策略:先分段摘要,再对摘要进行二次综合", + "C": "多次重复发送同一文档以「加深」模型记忆", + "D": "将文档拆分后并行发送给多个独立 LLM 实例,取第一个返回的结果" + }, + "answer": "B", + "explanation": "Map-Reduce 是长文档处理的经典模式:Map 阶段将文档切分为多个 chunk 并行生成局部摘要,Reduce 阶段将所有局部摘要合并生成最终摘要。这种方法既保证了信息覆盖的完整性,又避免了上下文溢出。A 选项会丢失大量关键信息;C 选项重复发送并不能增加有效信息量,反而浪费 token;D 选项只取第一个结果忽略了其他分段的信息,且无综合步骤。", + "source": null, + "related": [] + }, + { + "id": "sc-003", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "harness-engineering", + "ai-agent" + ], + "question": "在 Agent 编排框架中,ReAct 模式与 Plan-and-Execute 模式的核心区别是什么?", + "options": { + "A": "ReAct 模式不支持工具调用,仅用于对话场景", + "B": "ReAct 采用逐步推理-行动的循环,Plan-and-Execute 先制定完整计划再逐步执行", + "C": "Plan-and-Execute 模式不支持在执行过程中动态调整计划", + "D": "ReAct 模式只能处理单步任务,无法处理多步骤复杂任务" + }, + "answer": "B", + "explanation": "ReAct(Reasoning + Acting)的核心是 Thought → Action → Observation 的逐步循环,每一步根据当前观察决定下一步行动,适合需要动态调整的探索型任务。Plan-and-Execute 则先让 LLM 生成一个完整的多步计划,再由执行器逐步实施,适合目标明确、步骤可预知的任务。A 错误:ReAct 广泛用于工具调用场景。C 错误:大多数 Plan-and-Execute 实现支持在执行中根据反馈修改计划。D 错误:ReAct 的循环结构天然支持多步任务。", + "source": null, + "related": [] + }, + { + "id": "sc-004", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "ai-agent" + ], + "question": "在多 Agent 系统中,「共享黑板(Blackboard)」通信模式的核心特征是?", + "options": { + "A": "所有 Agent 通过点对点消息直接通信,无需中间媒介", + "B": "设置一个中心调度器,由它决定哪个 Agent 在何时执行任务", + "C": "Agent 通过读写共享的全局状态空间进行间接通信,无需直接对接", + "D": "所有 Agent 共享同一个 LLM 实例,通过内存隔离实现并行" + }, + "answer": "C", + "explanation": "黑板(Blackboard)模式源自人工智能经典架构,核心思想是维护一个共享的全局数据结构(黑板),各 Agent 独立地读取黑板上的信息、执行推理并将结果写回黑板,从而实现间接的松耦合通信。A 描述的是点对点(Peer-to-Peer)通信模式;B 描述的是中心调度(Central Orchestrator)模式;D 描述的是模型共享架构,与通信模式无关。黑板模式的优势在于 Agent 之间不需要了解彼此的存在,便于扩展和替换。", + "source": null, + "related": [] + }, + { + "id": "sc-005", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "mcp", + "tool-calling" + ], + "question": "在 MCP(Model Context Protocol)协议中,以下关于 Tool、Resource、Prompt 三个核心要素的描述,正确的是?", + "options": { + "A": "Tool 是 LLM 可调用的外部函数,Resource 是服务端提供的数据源,Prompt 是预定义的提示词模板", + "B": "Tool 和 Resource 的区别仅在于命名不同,功能完全相同", + "C": "Prompt 是 LLM 生成的输出格式约束,Tool 是客户端向服务端发送的请求", + "D": "Resource 只能返回文本数据,不支持图片或结构化数据" + }, + "answer": "A", + "explanation": "MCP 协议定义了三个核心原语:Tool 代表 LLM 可以调用的外部函数(如 API 调用、代码执行),由客户端发起调用;Resource 是服务端暴露的数据源(如文件、数据库记录),通常由客户端按 URI 读取;Prompt 是服务端提供的预定义提示词模板,供客户端在构造请求时使用。B 错误:Tool 涉及执行操作,Resource 涉及数据读取,语义和实现不同。C 错误:Prompt 不是输出约束,而是输入模板。D 错误:Resource 支持多种内容类型,包括图片和结构化数据。", + "source": null, + "related": [] + }, + { + "id": "sc-006", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "permission" + ], + "question": "在设计 LLM Agent 的权限治理体系时,「最小权限原则(Principle of Least Privilege)」的最佳实践是?", + "options": { + "A": "为 Agent 配置管理员权限,确保它在任何场景下都能顺利完成任务", + "B": "每次工具调用时动态申请权限,根据当前任务上下文授予刚好够用的最小权限集", + "C": "所有工具调用一律禁止,需要人工逐条审批后才能执行", + "D": "将权限配置写死在代码中,运行时不允许任何变更" + }, + "answer": "B", + "explanation": "最小权限原则要求 Agent 仅获得完成当前任务所必需的最小权限。动态权限申请是最佳实践——Agent 在执行每个工具调用前声明其意图和所需权限,系统根据任务上下文和安全策略决定是否授权。这既保证了安全性(避免过度授权),又保证了灵活性(不同任务授予不同权限)。A 违反最小权限原则,过度授权带来安全风险;C 过于严格会阻塞正常工作流,适用于高风险操作而非所有操作;D 的静态权限无法适应不同任务的差异化需求。", + "source": null, + "related": [] + }, + { + "id": "sc-007", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "pluggable-config" + ], + "question": "在可插拔配置架构中,Provider 抽象层的核心价值是什么?", + "options": { + "A": "让系统只能使用特定厂商的 LLM,确保输出质量一致", + "B": "统一不同 LLM 供应商的接口差异,使上层应用无需感知底层模型切换", + "C": "自动生成 Prompt 模板,不需要人工编写任何提示词", + "D": "为每个 LLM 供应商创建独立的应用程序,互不干扰" + }, + "answer": "B", + "explanation": "Provider 抽象层(如 LiteLLM、OpenAI 兼容接口)的核心价值在于屏蔽不同 LLM 供应商的 API 差异(请求格式、认证方式、响应结构等),为上层应用提供统一的调用接口。当需要切换底层模型时(如从 GPT-4 切换到 Claude),只需修改配置而不必改动业务代码。A 与可插拔理念相悖;C 属于 Prompt Engineering 工具的功能,与 Provider 抽象层无关;D 违背了抽象层「统一接口」的初衷。", + "source": null, + "related": [] + }, + { + "id": "sc-008", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "guardrail" + ], + "question": "以下关于 LLM Guardrail 中 PII(个人可识别信息)检测的描述,正确的是?", + "options": { + "A": "PII 检测只需要在输入端执行,输出端无需处理,因为 LLM 不会凭空生成个人信息", + "B": "PII 检测应同时覆盖输入和输出,且需要支持正则匹配与上下文感知的 NER 模型结合", + "C": "PII 检测会显著降低系统性能,因此在生产环境中应该完全跳过", + "D": "PII 检测只适用于金融行业,其他行业不需要" + }, + "answer": "B", + "explanation": "PII 检测需要在输入和输出两个方向上都进行。输入端防止敏感信息泄露给 LLM,输出端防止 LLM 在生成内容中包含用户提供的或训练数据中的敏感信息。技术实现上,正则匹配适合检测格式固定的 PII(如手机号、身份证号),NER 模型适合检测上下文相关的 PII(如人名、地址),两者结合效果最佳。A 错误:LLM 可能通过 RAG 或记忆机制在输出中泄露输入中的 PII;C 错误:PII 检测是合规要求,性能问题可以通过异步检测、缓存等手段优化;D 错误:所有处理用户数据的行业都应考虑 PII 检测。", + "source": null, + "related": [] + }, + { + "id": "sc-009", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "llm-ops" + ], + "question": "在 LLM 应用的可靠性保障中,使用 JSON Mode / Structured Output 的主要目的是什么?", + "options": { + "A": "提升 LLM 的推理速度,减少响应延迟", + "B": "确保 LLM 输出符合预定义的结构化格式,降低下游解析失败的风险", + "C": "限制 LLM 只能回答特定领域的问题", + "D": "减少 LLM 的幻觉问题,提升事实准确性" + }, + "answer": "B", + "explanation": "JSON Mode / Structured Output(如 OpenAI 的 response_format、Function Calling 的参数约束)的核心目的是确保 LLM 的输出符合预定义的 schema 结构,从而让下游系统可以可靠地解析和处理输出。这解决了 LLM 输出格式不稳定导致的集成问题。A 错误:结构化输出约束甚至可能略微增加延迟(需要额外的格式校验);C 错误:结构化输出约束的是格式而非内容领域;D 错误:结构化输出不直接解决幻觉问题——模型仍可能在有效 JSON 中生成虚假内容,解决幻觉需要 RAG、事实校验等其他手段。", + "source": null, + "related": [] + }, + { + "id": "sc-010", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "llm-ops" + ], + "question": "当 LLM 服务出现临时性故障(如 rate limit 或 503 错误)时,以下哪种重试策略最为合理?", + "options": { + "A": "立即无限次重试,直到成功为止", + "B": "采用指数退避(Exponential Backoff)+ 抖动(Jitter)策略,并设置最大重试次数", + "C": "不进行任何重试,直接将错误返回给用户", + "D": "每次固定间隔 1 秒重试,重试 100 次" + }, + "answer": "B", + "explanation": "指数退避 + 抖动是处理临时性故障的标准策略。指数退避(每次重试间隔翻倍,如 1s → 2s → 4s → 8s)避免了在服务恢复前持续施压;抖动(在退避间隔上加入随机偏移)防止多个客户端同时重试造成「惊群效应(Thundering Herd)」。设置最大重试次数可避免无限循环。A 会加剧服务端压力并可能触发更严厉的限流;C 无法应对可恢复的临时故障,用户体验差;D 的固定间隔策略在大规模部署时容易引发同步重试风暴。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/interview-prep/ai-engineering/true_false.json b/topics/interview-prep/ai-engineering/true_false.json new file mode 100644 index 0000000..d2effe5 --- /dev/null +++ b/topics/interview-prep/ai-engineering/true_false.json @@ -0,0 +1,78 @@ +{ + "topic": "ai-engineering", + "type": "true_false", + "schema_version": "1.0.0", + "generated": "2026-09-09T10:00:00+08:00", + "questions": [ + { + "id": "tf-001", + "type": "true_false", + "difficulty": 3, + "tags": [ + "context-engineering", + "ai-agent" + ], + "question": "在 RAG(检索增强生成)架构中,向量数据库的语义检索结果可以完全替代 LLM 的参数化知识,因此 RAG 系统不需要依赖 LLM 自身的预训练知识。", + "answer": false, + "explanation": "这个说法是错误的。RAG 的核心设计理念是「检索 + 生成」协同工作,而非替代关系。向量数据库提供的语义检索结果作为上下文(context)注入 prompt,但 LLM 仍然需要利用自身的参数化知识来理解检索结果、进行推理、整合信息并生成最终回答。检索结果通常只是片段化的、可能包含噪声的参考资料,LLM 的预训练知识起到语义理解、逻辑推理和知识补全的关键作用。两者是互补关系,缺一不可。", + "source": null, + "related": [] + }, + { + "id": "tf-002", + "type": "true_false", + "difficulty": 4, + "tags": [ + "harness-engineering", + "ai-agent" + ], + "question": "ReAct 模式与 Plan-and-Execute 模式的核心区别在于:ReAct 采用「思考→行动→观察」的即时循环,而 Plan-and-Execute 先生成完整计划再逐步执行,因此 Plan-and-Execute 更适合需要多步回溯和动态调整的复杂任务。", + "answer": false, + "explanation": "这个说法是错误的。虽然前半部分对两种模式的描述是正确的——ReAct 是 Thought→Action→Observation 的交替循环,Plan-and-Execute 是先规划再执行——但结论恰好相反。ReAct 的逐步交替模式天然支持每一步根据观察结果动态调整策略,更适合需要频繁回溯和动态调整的任务。Plan-and-Execute 的计划一旦生成,调整成本较高(需要重新规划),更适合目标明确、步骤可预判的任务。在实际工程中,Plan-and-Execute 通常需要加入 re-planning 机制来弥补这一不足。", + "source": null, + "related": [] + }, + { + "id": "tf-003", + "type": "true_false", + "difficulty": 3, + "tags": [ + "mcp", + "tool-calling" + ], + "question": "MCP(Model Context Protocol)协议定义的三大核心要素是 Tool、Resource 和 Prompt,其中 Tool 允许服务端暴露可执行函数供 LLM 调用,Resource 提供只读的数据访问接口,Prompt 则定义了预构建的交互模板。", + "answer": true, + "explanation": "这个说法是正确的。MCP 协议确实定义了这三个核心要素:Tool 是服务端提供的可执行函数(如查询数据库、调用 API),LLM 可以主动调用它来完成操作;Resource 是只读的数据源(如文件内容、数据库记录),为 LLM 提供上下文信息但不允许修改;Prompt 是预定义的 prompt 模板,用于标准化交互模式。这种设计将「可执行操作」「只读数据」和「交互模板」三者清晰分离,各有不同的权限级别和交互语义,是 MCP 安全模型的基础。", + "source": null, + "related": [] + }, + { + "id": "tf-004", + "type": "true_false", + "difficulty": 2, + "tags": [ + "permission", + "guardrail" + ], + "question": "在 AI Agent 的权限治理中,最小权限原则(Principle of Least Privilege)意味着 Agent 在每个任务阶段只应被授予完成该任务所必需的最低限度权限,而不是在整个会话期间拥有固定的全量权限。", + "answer": true, + "explanation": "这个说法是正确的。最小权限原则是 AI Agent 安全设计的基石。具体实施中,Agent 的权限应该随任务阶段动态调整:例如在数据读取阶段只授予只读权限,在需要写入时才临时提升权限,任务完成后立即收回。这与传统的「全量授权」模式有本质区别。Human-in-the-Loop 机制通常配合最小权限使用——当 Agent 需要超出当前权限的操作时,请求人类审批而非默认放行。这种设计显著降低了 Agent 误操作或被攻击后的损害范围。", + "source": null, + "related": [] + }, + { + "id": "tf-005", + "type": "true_false", + "difficulty": 4, + "tags": [ + "guardrail", + "llm-ops" + ], + "question": "当 LLM 输出需要作为结构化数据被下游系统消费时,使用 JSON Mode(或 Structured Output)配合 JSON Schema 校验,可以完全消除输出格式错误的问题,无需额外的容错处理。", + "answer": false, + "explanation": "这个说法是错误的。JSON Mode / Structured Output 虽然能大幅提升输出格式的合规率,但不能「完全消除」问题。原因包括:(1)即使 JSON 语法正确,语义层面仍可能不符合业务预期(如枚举值在合法范围内但不符合业务逻辑);(2)不同模型对 JSON Mode 的支持程度不同,部分模型可能在边界情况下仍然输出不合规内容;(3)Token 限制可能导致 JSON 被截断;(4)网络传输和序列化过程也可能引入问题。工程实践中,JSON Schema 校验应作为第一道防线,之后仍需业务层的容错处理(如重试、fallback、日志告警等),形成多层防御体系。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/interview-prep/database-advanced/code_reading.json b/topics/interview-prep/database-advanced/code_reading.json new file mode 100644 index 0000000..3ac2f58 --- /dev/null +++ b/topics/interview-prep/database-advanced/code_reading.json @@ -0,0 +1,309 @@ +{ + "topic": "database-advanced", + "type": "code_reading", + "schema_version": "1.0.0", + "generated": "2026-09-09T00:00:00+08:00", + "questions": [ + { + "id": "cr-001", + "type": "code_reading", + "difficulty": 4, + "tags": [ + "mysql", + "bplus-tree", + "composite-index", + "covering-index", + "index" + ], + "question": "分析以下建表语句和查询,判断索引使用情况", + "code": "CREATE TABLE orders (\n id BIGINT PRIMARY KEY AUTO_INCREMENT,\n user_id INT NOT NULL,\n product_id INT NOT NULL,\n status TINYINT DEFAULT 0,\n amount DECIMAL(10,2),\n created_at DATETIME DEFAULT CURRENT_TIMESTAMP,\n INDEX idx_user_product_status (user_id, product_id, status)\n) ENGINE=InnoDB;\n\n-- 查询 Q1\nSELECT user_id, product_id, status FROM orders\nWHERE user_id = 100 AND product_id = 500;\n\n-- 查询 Q2\nSELECT user_id, product_id, status, amount FROM orders\nWHERE product_id = 500 AND user_id = 100;\n\n-- 查询 Q3\nSELECT user_id, product_id, status FROM orders\nWHERE user_id = 100 AND status = 1;", + "language": "sql", + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "查询 Q1 中,MySQL 优化器能否将 WHERE 条件顺序自动调整以命中联合索引 (user_id, product_id, status) 的最左前缀?", + "options": { + "A": "不能,WHERE 子句顺序必须与索引列顺序完全一致", + "B": "能,优化器会根据 WHERE 条件自动匹配索引列,不受书写顺序影响", + "C": "不能,MySQL 只会按索引从左到右扫描,不处理 WHERE 顺序", + "D": "取决于 SQL 模式 setting" + }, + "answer": "B", + "explanation": "MySQL 优化器会分析 WHERE 子句中的等值条件,自动将 user_id=100 和 product_id=500 与联合索引的前两列匹配,不受 SQL 中书写顺序的影响。Q1 的两个等值条件恰好连续覆盖索引前两列 (user_id, product_id),因此可以完全命中索引前缀,type 为 ref。" + }, + { + "index": 2, + "type": "short_answer", + "question": "Q2 与 Q1 的查询条件相同(只是书写顺序不同),且 Q2 多查了 amount 列。请分析:(a) Q2 的索引扫描类型是什么?(b) Q1 是覆盖索引查询吗?Q2 呢?", + "answer": "(a) Q2 的扫描类型是 ref,与 Q1 相同。(b) Q1 是覆盖索引查询——其 SELECT 的列 user_id、product_id、status 全部包含在 idx_user_product_status 中,无需回表。Q2 不是覆盖索引查询——amount 不在索引中,需要通过聚簇索引回表取值。", + "keywords": [ + "ref", + "覆盖索引", + "回表", + "Extra: Using index" + ], + "scoring_rubric": "Q2 扫描类型正确得1分,Q1 覆盖索引判断正确得1分,Q2 不是覆盖索引并解释回表得1分", + "explanation": "虽然 WHERE 子句中 product_id 写在 user_id 前面,但 MySQL 优化器会自动调整匹配顺序。Q1 的 SELECT 列 (user_id, product_id, status) 全在联合索引中,EXPLAIN 的 Extra 会显示 'Using index'(覆盖索引)。Q2 多了 amount 列,不在索引中,必须回表到聚簇索引获取数据,Extra 显示 'Using index condition' 或无 Using index。" + }, + { + "index": 3, + "type": "short_answer", + "question": "查询 Q3 的 WHERE 条件跳过了联合索引的第二列 product_id(只有 user_id=100 和 status=1),MySQL 能否同时利用索引中的 user_id 和 status 列?请分析其索引使用情况。", + "answer": "MySQL 只能利用索引的第一列 user_id 进行范围扫描(type=ref),将 user_id=100 的所有行取出来后,在 server 层对 status=1 做过滤。索引的第三列 status 无法直接用于索引查找,因为跳过了第二列 product_id,违反了最左前缀原则。", + "keywords": [ + "最左前缀", + "索引截断", + "server 层过滤" + ], + "scoring_rubric": "提到只利用 user_id 列得1分,提到最左前缀原则得1分,提到需要 server 层过滤得1分", + "explanation": "联合索引 (user_id, product_id, status) 要求从最左列开始连续使用。Q3 的条件 user_id=100 AND status=1 中间跳过了 product_id,MySQL 只能使用索引的 user_id 部分进行查找,EXPLAIN 中 key_len 只反映 user_id 的长度(4 字节 INT)。剩余的 status=1 条件在 server 层通过 'Using where' 过滤,而非在存储引擎层通过索引过滤。如果 status 选择性很高,可以考虑创建 idx_user_status(user_id, status) 辅助索引。" + } + ], + "explanation": "本题考察 B+树联合索引的三个核心概念:(1) 最左前缀原则——索引列必须从左到右连续使用,但 WHERE 条件的书写顺序不影响优化器匹配;(2) 覆盖索引——当查询列全部包含在索引中时,可避免回表,性能最优;(3) 索引列跳跃——跳过中间列会导致后续列无法参与索引查找,只能退化为 server 层过滤。InnoDB 的 B+树二级索引叶子节点存储的是主键值,非覆盖索引查询必须通过主键回表到聚簇索引获取完整行数据。", + "source": null, + "related": [] + }, + { + "id": "cr-002", + "type": "code_reading", + "difficulty": 3, + "tags": [ + "mysql", + "explain", + "index" + ], + "question": "分析以下 EXPLAIN 执行计划输出,判断查询性能", + "code": "EXPLAIN SELECT o.id, o.amount, u.name\nFROM orders o\nJOIN users u ON o.user_id = u.id\nWHERE o.status = 1\n AND o.created_at >= '2026-01-01'\n AND u.city = 'Beijing';\n\n-- 输出结果(简化):\n+----+------+-------+------------------+---------+------+----------+-------+\n| id | type | table | possible_keys | key | rows | filtered | Extra |\n+----+------+-------+------------------+---------+------+----------+-------+\n| 1 | ALL | o | NULL | NULL | 500K | 10.00 | Using where |\n| 1 | ref | u | PRIMARY | PRIMARY | 1 | 25.00 | Using where |\n+----+------+-------+------------------+---------+------+----------+-------+", + "language": "sql", + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "options": { + "A": "驱动表是 u,被驱动表是 o,因为 u 的 rows=1 更小", + "B": "驱动表是 o,被驱动表是 u,优化器选择了 o 作为驱动表", + "C": "无法确定驱动表,取决于 MySQL 版本", + "D": "两张表同时扫描,不存在驱动/被驱动关系" + }, + "question": "根据 EXPLAIN 输出,哪张表是驱动表(第一张被访问的表)?", + "answer": "B", + "explanation": "EXPLAIN 输出中 id=1 相同的情况下,输出顺序即为表的访问顺序。orders 表(别名 o)排在第一行,是驱动表。虽然 users 表的 rows=1 更小,但优化器认为先全表扫描 orders 表再对每行用主键查 users 表的总体代价更低(这是没有索引支持 o 表过滤条件时的无奈选择)。" + }, + { + "index": 2, + "type": "single_choice", + "options": { + "A": "orders 表缺少 status 和 created_at 的联合索引,应创建 INDEX(status, created_at)", + "B": "users 表缺少 city 索引,应创建 INDEX(city)", + "C": "应使用 STRAIGHT_JOIN 强制 u 表作为驱动表", + "D": "增大 innodb_buffer_pool_size 即可解决全表扫描问题" + }, + "question": "orders 表 type=ALL 导致全表扫描 50 万行,最有效的优化措施是什么?", + "answer": "A", + "explanation": "orders 表 type=ALL 且 possible_keys=NULL,说明没有可用索引。status 和 created_at 都是 WHERE 条件列,创建联合索引 (status, created_at) 可以让优化器利用索引定位 status=1 且 created_at >= '2026-01-01' 的行,避免全表扫描。即使 users 表缺少 city 索引(确实也缺),但 orders 表扫描 50 万行是当前最大的性能瓶颈。" + }, + { + "index": 3, + "type": "short_answer", + "question": "users 表的 EXPLAIN 显示 filtered=25.00,这意味着什么?优化 users 表查询的最佳方案是什么?", + "answer": "filtered=25.00 表示通过索引(PRIMARY)找到的行中,只有 25% 满足 u.city='Beijing' 的 WHERE 条件。即每次对 orders 表的一行用主键查找 users 表时,返回的行还要在 server 层过滤掉 75%。最佳方案是创建 INDEX(city) 或 INDEX(city, name),使 city='Beijing' 能直接在索引层面完成过滤,减少回表。", + "keywords": [ + "filtered", + "server 层过滤", + "city 索引" + ], + "scoring_rubric": "正确解释 filtered 含义得1分,给出创建 city 索引建议得1分", + "explanation": "EXPLAIN 中的 filtered 列表示存储引擎返回的行中,经过 WHERE 条件过滤后保留的百分比。users 表通过主键 ref 查找返回 1 行,其中约 25% 满足 city='Beijing'。虽然单次过滤代价不高(因为 rows=1),但如果 orders 表有大量行,累积的 server 层过滤开销依然可观。创建 (city) 或 (city, name) 索引后,type 可从 ref 优化为更精准的 ref,filtered 接近 100%。" + } + ], + "explanation": "本题考察 EXPLAIN 执行计划的核心字段分析:(1) type——ALL 表示全表扫描,ref 表示索引查找;(2) rows——估算扫描行数,直接影响 I/O 开销;(3) filtered——存储引擎返回行中满足 WHERE 条件的百分比;(4) possible_keys 与 key——可用索引和实际选择的索引;(5) Extra 中的 Using where 表示需要在 server 层额外过滤。优化思路优先级:先消除 ALL 全表扫描,再关注 filtered 过低的问题。", + "source": null, + "related": [] + }, + { + "id": "cr-003", + "type": "code_reading", + "difficulty": 5, + "tags": [ + "mysql", + "slow-query", + "explain", + "index", + "bplus-tree" + ], + "question": "分析以下慢查询日志中的 SQL 及其执行计划,找出性能问题并给出优化方案", + "code": "-- 慢查询日志 (long_query_time = 1)\n# Query_time: 12.345 Lock_time: 0.001 Rows_sent: 15 Rows_examined: 3200000\nSELECT p.id, p.title, p.price, c.name AS category,\n COUNT(DISTINCT r.user_id) AS review_count\nFROM products p\nLEFT JOIN product_categories pc ON p.id = pc.product_id\nLEFT JOIN categories c ON pc.category_id = c.id\nLEFT JOIN reviews r ON r.product_id = p.id\nWHERE p.status = 'active'\n AND p.price BETWEEN 50 AND 200\n AND (c.name = 'Electronics' OR c.name = 'Books')\nGROUP BY p.id, p.title, p.price, c.name\nHAVING review_count >= 5\nORDER BY review_count DESC\nLIMIT 20;\n\n-- EXPLAIN 结果:\n+----+-------------+-------+------+---------------+------+---------+------+--------+----------------------------------------------+\n| id | select_type | table | type | possible_keys | key | key_len | rows | filtered | Extra |\n+----+-------------+-------+------+---------------+------+---------+------+--------+----------------------------------------------+\n| 1 | SIMPLE | p | ALL | NULL | NULL | NULL | 800K | 1.25 | Using where; Using temporary; Using filesort|\n| 1 | SIMPLE | pc | ALL | NULL | NULL | NULL | 1.2M | 10.00 | Using join buffer (block nested loop) |\n| 1 | SIMPLE | c | eq_ref| PRIMARY | PRIMARY| 4 | 1 | 20.00 | Using where |\n| 1 | SIMPLE | r | ALL | NULL | NULL | NULL | 200K | 1.00 | Using join buffer (block nested loop) |\n+----+-------------+-------+------+---------------+------+---------+------+--------+----------------------------------------------+", + "language": "sql", + "sub_questions": [ + { + "index": 1, + "type": "short_answer", + "question": "该查询扫描了约 320 万行但只返回 15 行,列出至少 3 个导致性能低下的原因。", + "answer": "1) products 表 type=ALL 全表扫描 80 万行,缺少 status 和 price 的索引,BETWEEN 条件无法走索引。2) product_categories 表 type=ALL 全表扫描 120 万行,缺少 product_id 外键索引,导致嵌套循环使用 Block Nested Loop 而非索引关联。3) reviews 表同样全表扫描 20 万行,缺少 product_id 索引。4) GROUP BY 产生 Using temporary 和 Using filesort,对大量中间结果进行排序和临时表操作,开销巨大。", + "keywords": [ + "全表扫描", + "缺少索引", + "Block Nested Loop", + "Using temporary", + "Using filesort" + ], + "scoring_rubric": "每个有效原因得1分,最多3分", + "explanation": "EXPLAIN 中三张表(p、pc、r)type=ALL 且 possible_keys=NULL,说明完全缺少索引。products 表有 80 万行,对每行都要在 product_categories 中扫描 120 万行的笛卡尔积(Block Nested Loop),再对每条匹配在 reviews 中扫描 20 万行,总扫描量呈乘积关系增长,导致 Rows_examined 高达 320 万。加上 GROUP BY + COUNT(DISTINCT) 的排序和去重开销,以及 HAVING 后置过滤,实际有效工作量远超扫描行数。" + }, + { + "index": 2, + "type": "short_answer", + "question": "请给出具体的索引创建方案和 SQL 改写建议,使查询执行时间从 12 秒降到 1 秒以内。", + "answer": "索引方案:\n1) CREATE INDEX idx_products_status_price ON products(status, price); -- 覆盖 status 等值 + price 范围\n2) CREATE INDEX idx_pc_product_category ON product_categories(product_id, category_id); -- 覆盖 JOIN + category 过滤\n3) CREATE INDEX idx_reviews_product ON reviews(product_id, user_id); -- 覆盖 JOIN + COUNT(DISTINCT user_id)\n\nSQL 改写建议:\n- 将 WHERE 中 c.name IN ('Electronics','Books') 提前到 JOIN products 前作为子查询过滤,先通过 categories 表筛选 category_id,再 JOIN product_categories,减少扫描量。\n- 考虑将 LEFT JOIN 改为 INNER JOIN(如果业务允许),因为 WHERE 条件中引用了 c.name,LEFT JOIN 实际已等效于 INNER JOIN。", + "keywords": [ + "联合索引", + "覆盖索引", + "子查询下推", + "INNER JOIN" + ], + "scoring_rubric": "创建 idx_products_status_price 得1分,创建 idx_pc_product_category 得1分,创建 idx_reviews_product 得1分,SQL 改写建议得1分,最多4分", + "explanation": "索引设计遵循最左前缀原则:idx_products_status_price 让 status='active' 等值查找后在索引层内做 price BETWEEN 50 AND 200 的范围扫描,避免全表扫描。idx_pc_product_category 是覆盖索引,JOIN 和 category_id 过滤都在索引层完成。idx_reviews_product 覆盖 reviews 表的 JOIN 条件和 COUNT(DISTINCT user_id)。SQL 改写的核心思路是先缩小驱动表的扫描范围——通过子查询先筛选出 Electronics 和 Books 的 category_id,再反查 product_categories,将 120 万行扫描大幅缩减。" + }, + { + "index": 3, + "type": "single_choice", + "options": { + "A": "增大 sort_buffer_size 到 64MB,避免 filesort 写磁盘", + "B": "在 products 表上创建 INDEX(status, price, id) 使 GROUP BY p.id 可以利用索引排序", + "C": "将 COUNT(DISTINCT r.user_id) 改为 COUNT(r.user_id) 并在应用层去重", + "D": "禁用 GROUP BY 的 ONLY_FULL_GROUP_BY 模式" + }, + "question": "对于该查询的 GROUP BY + ORDER BY review_count DESC + LIMIT 20,以下哪种优化思路最合理?", + "answer": "B", + "explanation": "当前查询需要对大量中间结果做 GROUP BY 并统计 review_count,再排序取 Top 20。如果先通过索引大幅减少参与 JOIN 的行数(从 80 万降到几千),Using temporary 和 Using filesort 的代价会显著降低。创建 INDEX(status, price, id) 不是为了让 GROUP BY p.id 利用索引排序(因为有 JOIN 和聚合函数,MySQL 不会这么做),而是为了让 products 表的扫描从 ALL 变为 range,极大减少参与后续 JOIN 的行数。选项 A 的 sort_buffer_size 只影响排序内存,不改变扫描量;选项 C 改变了语义;选项 D 是危险操作且不解决根本问题。" + } + ], + "explanation": "本题是一个典型的慢查询优化场景。核心问题链:products 表缺少索引导致 80 万行全表扫描 → 每行在 product_categories 中做 Block Nested Loop 扫描 120 万行 → 每行在 reviews 中再扫描 20 万行 → 巨大的中间结果集做 GROUP BY + COUNT(DISTINCT) + ORDER BY。优化策略分三层:(1) 创建精准索引消除全表扫描;(2) SQL 改写缩小驱动表数据量;(3) 考虑业务层面的折中(如限制查询范围、异步统计等)。在实际生产中,还需配合慢查询监控、pt-query-digest 分析和执行计划对比来验证优化效果。", + "source": null, + "related": [] + }, + { + "id": "cr-004", + "type": "code_reading", + "difficulty": 5, + "tags": [ + "mysql", + "lock", + "deadlock" + ], + "question": "分析以下并发事务场景,判断是否会发生死锁以及死锁产生的原因", + "code": "-- 表结构\nCREATE TABLE accounts (\n id INT PRIMARY KEY,\n balance DECIMAL(10,2) NOT NULL,\n INDEX idx_balance (balance)\n) ENGINE=InnoDB;\n\n-- 初始数据: id=1, balance=1000; id=2, balance=2000\n\n-- ========== 事务时间线 ==========\n-- T1 (2026-09-09 10:00:01.000)\nBEGIN;\nUPDATE accounts SET balance = balance - 100 WHERE id = 1;\n-- T1 获取 id=1 行的 X 锁\n\n-- T2 (2026-09-09 10:00:01.050)\nBEGIN;\nUPDATE accounts SET balance = balance + 100 WHERE id = 2;\n-- T2 获取 id=2 行的 X 锁\n\n-- T1 (2026-09-09 10:00:01.100)\nUPDATE accounts SET balance = balance + 100 WHERE id = 2;\n-- T1 尝试获取 id=2 行的 X 锁 → 阻塞(被 T2 持有)\n\n-- T2 (2026-09-09 10:00:01.150)\nUPDATE accounts SET balance = balance - 100 WHERE id = 1;\n-- T2 尝试获取 id=1 行的 X 锁 → 检测到死锁!\n\n-- ===== 另一个场景 (场景 B) =====\n-- T3 (事务)\nBEGIN;\nUPDATE accounts SET balance = balance - 50 WHERE balance = 1000;\n-- 使用二级索引 idx_balance 查找 balance=1000 的行\n\n-- T4 (事务)\nBEGIN;\nUPDATE accounts SET balance = balance - 50 WHERE balance = 2000;\n-- 使用二级索引 idx_balance 查找 balance=2000 的行\n\n-- T3\nUPDATE accounts SET balance = balance - 50 WHERE id = 1;\n-- T3 尝试获取 id=1 的 X 锁\n\n-- T4\nUPDATE accounts SET balance = balance - 50 WHERE id = 2;\n-- T4 尝试获取 id=2 的 X 锁", + "language": "sql", + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "options": { + "A": "场景 A 会死锁,场景 B 不会死锁", + "B": "场景 A 会死锁,场景 B 也会死锁", + "C": "场景 A 不会死锁,场景 B 会死锁", + "D": "两个场景都不会死锁" + }, + "question": "场景 A 和场景 B 分别会发生死锁吗?", + "answer": "B", + "explanation": "场景 A 是经典的交叉锁死锁:T1 持有 id=1 的 X 锁、请求 id=2 的 X 锁;T2 持有 id=2 的 X 锁、请求 id=1 的 X 锁,形成循环等待。场景 B 看似不会死锁(T3 用 balance=1000 更新 id=1,T4 用 balance=2000 更新 id=2),但 InnoDB 的二级索引加锁机制使得:T3 在 idx_balance 上获取 balance=1000 的记录锁后,还需要对聚簇索引的 id=1 加 X 锁;T4 同理。由于两个事务获取二级索引锁的顺序和聚簇索引锁的顺序可能不一致(取决于索引记录的物理顺序),实际上存在死锁风险。如果 idx_balance 上两条记录的顺序与 id 的顺序不同(如 balance=2000 对应 id=2 排在 balance=1000 对应 id=1 前面),就会产生交叉等待。" + }, + { + "index": 2, + "type": "short_answer", + "question": "在场景 A 中,InnoDB 的死锁检测机制是如何工作的?死锁发生后 MySQL 会如何处理?", + "answer": "InnoDB 使用等待图(wait-for graph)算法检测死锁:维护事务等待关系的有向图,当图中出现环路时即判定死锁。检测时机是每次加锁请求发生阻塞时(T2 执行 UPDATE WHERE id=1 时触发检测)。死锁发生后,InnoDB 选择回滚代价最小的事务(通常是 undo log 量最少的事务)进行自动回滚,并返回错误 ERROR 1213 (40001): Deadlock found。被回滚的事务可以由应用层捕获异常后重试。innodb_deadlock_detect 参数可控制是否开启检测(默认 ON),innodb_lock_wait_timeout 控制锁等待超时。", + "keywords": [ + "等待图", + "死锁检测", + "回滚", + "innodb_deadlock_detect", + "锁等待超时" + ], + "scoring_rubric": "提到等待图/环路检测得1分,提到选择回滚代价最小的事务得1分,提到应用层重试得1分", + "explanation": "InnoDB 的死锁检测基于等待图算法,每当一个事务请求锁被阻塞时,会检查当前等待关系图是否形成环路。场景 A 中:T1→T2(T1 等待 T2 释放 id=2 的锁),T2→T1(T2 等待 T1 释放 id=1 的锁),形成环路。InnoDB 立即检测到并回滚 T2(假设 T2 的 undo log 更少)。在高并发场景下,频繁的死锁检测会消耗 CPU,可以通过 innodb_deadlock_detect=OFF 配合锁等待超时来降低开销,但这需要应用层处理超时重试逻辑。" + }, + { + "index": 3, + "type": "short_answer", + "question": "请给出至少 3 种避免场景 A 中死锁的设计策略。", + "answer": "1) 固定加锁顺序:所有事务按 id 从小到大的顺序访问记录(先锁 id=1 再锁 id=2),消除循环等待条件。2) 使用 SELECT ... FOR UPDATE 显式加锁并统一在事务开始时一次性锁定所有需要的行,减少锁持有时间。3) 将大事务拆分为小事务,缩短锁持有时间窗口。4) 使用乐观锁(version 字段)替代悲观锁,通过 CAS 更新避免长时间持锁。5) 在业务层面确保转账操作的原子性——可以用单一 SQL UPDATE accounts SET balance = balance - 100 WHERE id = 1 AND balance >= 100 来避免两行分两次更新。", + "keywords": [ + "固定加锁顺序", + "缩短事务", + "乐观锁", + "一次性锁定" + ], + "scoring_rubric": "每种有效策略得1分,最多3分", + "explanation": "死锁发生的四个必要条件是:互斥、持有并等待、不可剥夺、循环等待。打破任何一个条件即可避免死锁。固定加锁顺序打破循环等待条件——所有事务按 id 排序后依次获取锁,最多只能形成单向等待链。缩短事务持有时间降低死锁概率(虽然不完全消除)。乐观锁通过应用层版本检查避免数据库层面的锁竞争。对于转账场景,最根本的方案是将两行更新合并为应用层逻辑控制的单条 SQL,避免跨行事务。" + } + ], + "explanation": "本题深入考察 InnoDB 锁机制和死锁分析。场景 A 是教科书级的交叉锁死锁,涉及行级 X 锁的互斥和循环等待。场景 B 揭示了一个更隐蔽的死锁来源:InnoDB 的二级索引加锁规则——UPDATE WHERE idx_col = val 会在二级索引上加记录锁,同时需要在聚簇索引(主键)上加 X 锁,两级索引的加锁顺序不一致可能导致死锁。实际生产中,还需要通过 SHOW ENGINE INNODB STATUS 查看 LATEST DETECTED DEADLOCK 区块来确认死锁详情,并结合 general_log 或 binlog 还原事务执行顺序。预防策略的核心思想是:统一加锁顺序、减少锁持有时间、降低锁粒度。", + "source": null, + "related": [] + }, + { + "id": "cr-005", + "type": "code_reading", + "difficulty": 4, + "tags": [ + "redis", + "cluster", + "slot", + "hybrid-persistence", + "bigkey" + ], + "question": "分析以下 Redis 集群配置和运维命令,判断集群状态和恢复方案", + "code": "# ===== Redis 集群节点信息 =====\n$ redis-cli -c -h 10.0.0.1 -p 7001 cluster nodes\n3a1b2c... 10.0.0.1:7001@17001 myself,master - 0 1694236800 1 connected 0-5460\n7d4e5f... 10.0.0.2:7002@17002 master - 0 1694236800 2 connected 5461-10922\nb8c9d0... 10.0.0.3:7003@17003 master - 0 1694236800 3 connected 10923-16383\ne1f2a3... 10.0.0.4:7004@17004 slave 3a1b2c... 0 1694236800 4 connected\na5b6c7... 10.0.0.5:7005@17005 slave 7d4e5f... 0 1694236800 5 connected\n\n# ===== 节点 7002 (10.0.0.2) 宕机后的集群状态 =====\n$ redis-cli -c -h 10.0.0.1 -p 7001 cluster info\ncluster_state:fail\ncluster_slots_assigned:16384\ncluster_slots_ok:10923\ncluster_slots_pfail:0\ncluster_slots_fail:5462\ncluster_known_nodes:6\ncluster_size:3\n\n# ===== 混合持久化配置 (redis.conf) =====\nappendonly yes\nappendfilename \"appendonly.aof\"\naof-use-rdb-preamble yes\naof-loading-trickle-size 4096\nsave 900 1\nsave 300 10\nsave 60 10000\n\n# ===== 恢复操作 =====\n# 1. 尝试恢复 7002 节点\n$ cp /var/lib/redis/7002/dump.rdb /var/lib/redis/7002/appendonlydir/appendonly.aof.1.rdb\n$ redis-server /etc/redis/7002.conf\n\n# 2. 查看恢复后的日志\n$ tail -20 /var/log/redis/7002.log\n[12345] 09 Sep 2026 10:05:00.123 * Done loading RDB preamble from AOF file\n[12345] 09 Sep 2026 10:05:00.234 * Starting BGSAVE for AOF\n[12345] 09 Sep 2026 10:05:00.235 * Background AOF rewrite started by pid 12346\n[12345] 09 Sep 2026 10:05:01.456 * Background AOF rewrite finished successfully", + "language": "redis", + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "options": { + "A": "slots 5461-10922 失效,且 7005 (slave) 会自动提升为 master 接管这些 slot", + "B": "slots 5461-10922 失效,但需要手动执行 CLUSTER FAILOVER 才能故障转移", + "C": "slots 5461-10922 失效,需要手动分配 slot 给其他 master 节点", + "D": "集群状态为 fail 只是告警,不影响正常的 slot 路由" + }, + "question": "节点 7002 (master, slots 5461-10922) 宕机后,集群状态 cluster_slots_fail=5462。以下分析正确的是?", + "answer": "A", + "explanation": "Redis Cluster 在 master 节点宕机后,其 slave 节点(7005)会在 cluster-node-timeout(默认 15 秒)后被标记为 PFAIL,随后被集群大多数 master 节点投票确认为 FAIL,自动提升为新的 master 并接管原 master 的所有 slot。从 cluster_nodes 输出看,7005 是 7002 的 slave,在正常配置下会自动故障转移。如果 cluster_slots_fail 持续显示 5462 而非恢复为 0,可能是因为 7005 也已下线或 cluster-node-timeout 还未到期。当 cluster_state=fail 时,整个集群对外拒绝服务(所有读写请求返回 CLUSTERDOWN 错误),直到所有 slot 都被正常 master 覆盖。" + }, + { + "index": 2, + "type": "short_answer", + "question": "配置 aof-use-rdb-preamble yes 意味着什么?它如何影响 Redis 的 AOF 文件格式和恢复流程?", + "answer": "aof-use-rdb-preamble yes 表示开启混合持久化(Redis 4.0+),AOF 文件的格式变为:前半部分是 RDB 格式的全量数据快照,后半部分是 AOF 格式的增量写命令。恢复时的流程为:(1) 先以 RDB 格式快速加载前半部分的全量数据(速度快,因为是二进制格式);(2) 再逐条重放后半部分的 AOF 增量命令(保证数据完整性)。日志中 'Done loading RDB preamble from AOF file' 正是第一步完成的标志。这种混合格式兼顾了 RDB 快速恢复和 AOF 数据完整的优点——纯 AOF 恢复需要逐条重放所有命令(慢),纯 RDB 会丢失最后一次快照后的数据。", + "keywords": [ + "混合持久化", + "RDB preamble", + "AOF", + "恢复流程" + ], + "scoring_rubric": "正确解释混合持久化格式得1分,正确描述恢复流程(先RDB后AOF)得1分,对比纯RDB/AOF的优劣得1分", + "explanation": "混合持久化是 Redis 4.0 引入的优化。传统模式下:RDB 快照恢复快但会丢失快照间隔内的数据;AOF 数据完整但恢复慢(需要逐条执行所有写命令)。混合持久化将两者结合:触发 BGSAVE 时,生成的 AOF 文件以 RDB 头部 + AOF 尾部的方式组织。从配置看,save 900 1 等规则触发 RDB 快照,而 appendonly yes 同时记录所有写命令。aof-use-rdb-preamble yes 确保 AOF 文件以 RDB 格式开头。恢复时 Redis 检测到 RDB preamble 后先快速加载(毫秒到秒级),再重放 AOF 增量(通常很短),兼顾速度和完整性。" + }, + { + "index": 3, + "type": "short_answer", + "question": "从 cluster_nodes 输出看,该集群有 3 master + 3 slave,slot 分配为 0-5460、5461-10922、10923-16383。如果需要扩展到 6 master 节点(每个 master 约 2730 个 slot),请说明 rehash 操作的大致流程。", + "answer": "Redis Cluster 扩容 rehash 流程:\n1) 新增 3 个 master 节点(7006/7007/7008)加入集群:CLUSTER MEET \n2) 为每个新节点分配 slot:从现有 3 个 master 各迁移约 1820 个 slot 给 3 个新节点,例如 CLUSTER ADDSLOTS 或使用 redis-cli --cluster reshard 工具\n3) 迁移过程中,源节点将对应 slot 的数据通过 MIGRATE 命令逐 key 迁移到目标节点\n4) 迁移期间:源节点收到不属于自己的 slot 的请求时,返回 ASK 重定向;目标节点收到未完成迁移的 key 时,返回 MOVED 重定向\n5) 所有 slot 迁移完成后,集群状态恢复正常,新节点开始接收对应 slot 的读写请求\n\n注意:生产环境建议使用 redis-cli --cluster reshard 交互式完成,它会自动处理 slot 状态标记和数据迁移。", + "keywords": [ + "rehash", + "slot 迁移", + "MIGRATE", + "reshard", + "ASK/MOVED" + ], + "scoring_rubric": "描述 CLUSTER MEET 加入节点得1分,描述 slot 分配和迁移过程得1分,提到 ASK/MOVED 重定向机制得1分", + "explanation": "Redis Cluster 的 slot 迁移是在线扩容的核心机制。每个 slot 在迁移前需要将状态从 NODE 设为 MIGRATING(源节点)和 IMPORTING(目标节点)。迁移过程中,源节点对正在迁移的 slot 的请求:如果 key 存在则正常处理,不存在则返回 ASK 重定向到目标节点。目标节点对 IMPORTING 状态的 slot:只有收到 ASKING 命令后的请求才会处理,否则返回 MOVED。这种设计确保了迁移期间集群的可用性——已迁移的 key 在新节点处理,未迁移的 key 在原节点处理,不会丢失数据。redis-cli --cluster reshard 工具封装了上述所有步骤。" + } + ], + "explanation": "本题综合考察 Redis Cluster 的运维能力和混合持久化机制。从 cluster_nodes 输出可以分析出集群拓扑:3 master 各负责约 1/3 的 slot(共 16384 个),每个 master 有一个 slave 用于高可用。故障转移流程涉及 PFAIL → FAIL 状态转换和 slave 提升。混合持久化(aof-use-rdb-preamble)是 Redis 4.0+ 的重要特性,将 RDB 的快速加载和 AOF 的数据完整性结合。扩容 rehash 是 Redis Cluster 运维中最复杂的操作之一,涉及 ASK/MOVED 重定向、MIGRATE 命令和 slot 状态管理。实际运维中还需关注 bigkey 问题——如果迁移的 slot 中包含大 key(如百万元素的 Hash/Set),MIGRATE 命令会长时间阻塞,建议先通过 redis-cli --bigkeys 识别并拆分大 key。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/interview-prep/database-advanced/fill_blank.json b/topics/interview-prep/database-advanced/fill_blank.json new file mode 100644 index 0000000..48bcc9b --- /dev/null +++ b/topics/interview-prep/database-advanced/fill_blank.json @@ -0,0 +1,216 @@ +{ + "topic": "database-advanced", + "type": "fill_blank", + "schema_version": "1.0.0", + "generated": "2026-09-09T00:00:00+08:00", + "questions": [ + { + "id": "fb-001", + "type": "fill_blank", + "difficulty": 2, + "tags": [ + "mysql", + "bplus-tree", + "index" + ], + "question": "在 InnoDB 中,主键索引的叶子节点直接存储______,因此基于主键查询效率最高;而二级索引的叶子节点存储的是主键值,查到后通常还需要______才能获取完整行数据。", + "answer": [ + "完整行数据", + "整行数据", + "完整的行数据", + "聚簇索引回表", + "回表" + ], + "answer_rule": "any", + "explanation": "InnoDB 使用聚簇索引(Clustered Index),主键索引的 B+ 树叶子节点存储的是完整的行数据,因此通过主键查询可以直接获取数据,无需额外的 IO。而二级索引(非聚簇索引)的叶子节点只存储主键值,查到主键后需要拿着主键回到聚簇索引中查找完整行数据,这个过程称为「回表」。回表会产生额外的随机 IO,是影响查询性能的重要因素。", + "source": null, + "related": [] + }, + { + "id": "fb-002", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "mysql", + "index", + "composite-index" + ], + "question": "联合索引 (a, b, c) 遵循最左前缀原则,以下查询中能使用该索引的有:WHERE a=1 AND b=2、WHERE a=1 AND c=3、WHERE b=2 AND c=3、WHERE a=1 ORDER BY b。其中 ______ 的查询无法使用索引(填查询条件序号,如 2、3)。", + "answer": [ + "2、3", + "2和3", + "b=2 AND c=3, a=1 AND c=3", + "第2个和第3个", + "WHERE a=1 AND c=3, WHERE b=2 AND c=3" + ], + "answer_rule": "any", + "explanation": "最左前缀原则要求查询条件从联合索引的最左列开始连续匹配。索引 (a, b, c):① WHERE a=1 AND b=2 命中前两列,可使用索引;② WHERE a=1 AND c=3 跳过了 b,只能用到 a 这一列,c 无法使用索引过滤;③ WHERE b=2 AND c=3 完全跳过了最左列 a,无法使用索引;④ WHERE a=1 ORDER BY b 命中 a 列,排序字段 b 在索引中紧跟 a,可避免 filesort。因此第 2、3 个查询无法有效利用索引。", + "source": null, + "related": [] + }, + { + "id": "fb-003", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "mysql", + "index", + "covering-index" + ], + "question": "当查询所需的所有列都包含在索引中时,MySQL 可以直接从索引获取数据而无需回表,这种技术称为______。在 EXPLAIN 的 Extra 列中会显示为 ______。", + "answer": [ + "覆盖索引", + "Using index" + ], + "answer_rule": "all", + "explanation": "覆盖索引(Covering Index)是指查询需要的所有字段都被某个索引所覆盖,InnoDB 只需扫描索引即可完成查询,避免了回表操作。此时 EXPLAIN 的 Extra 列会显示「Using index」,表示使用了覆盖索引。覆盖索引是优化查询性能的重要手段,能显著减少随机 IO。例如对索引 (name, age) 执行 SELECT name, age FROM users WHERE name = 'Tom',所有需要的列都在索引中,无需回表。", + "source": null, + "related": [] + }, + { + "id": "fb-004", + "type": "fill_blank", + "difficulty": 4, + "tags": [ + "mysql", + "index", + "bplus-tree" + ], + "question": "MySQL 5.6 引入的索引下推(Index Condition Pushdown,ICP)优化,将 ______ 下推到存储引擎层在索引扫描阶段完成过滤。在 EXPLAIN 的 Extra 列中,启用 ICP 时会显示 ______。", + "answer": [ + "WHERE 条件过滤", + "部分 WHERE 条件", + "索引不覆盖的过滤条件", + "Using index condition" + ], + "answer_rule": "all", + "explanation": "索引下推(ICP)是 MySQL 5.6 引入的优化。在没有 ICP 之前,存储引擎根据索引找到记录后回表,Server 层再进行 WHERE 过滤。ICP 将部分 WHERE 条件下推到存储引擎层,在索引扫描时就进行过滤,减少了回表次数和 Server 层的处理开销。当 ICP 生效时,EXPLAIN Extra 列显示「Using index condition」。注意 ICP 仅适用于 InnoDB 和 MyISAM,且只对联合索引起作用——因为单列索引的叶子节点已经包含了完整信息,不需要额外过滤。", + "source": null, + "related": [] + }, + { + "id": "fb-005", + "type": "fill_blank", + "difficulty": 2, + "tags": [ + "mysql", + "explain" + ], + "question": "EXPLAIN 执行计划的 type 列中,性能从好到差的大致顺序为:system > ______ > eq_ref > ref > range > index > ALL。其中 ______ 表示全表扫描,通常需要重点优化。", + "answer": [ + "const", + "ALL" + ], + "answer_rule": "all", + "explanation": "EXPLAIN 的 type 列表示 MySQL 在表中找到匹配行的方式。const 表示通过主键或唯一索引查找到单行(如 WHERE id=1),速度最快;eq_ref 表示对每一行的 JOIN 都从表中用主键/唯一索引查一行;ref 表示使用普通索引查找;range 表示索引范围扫描;index 表示全索引扫描;ALL 是全表扫描,性能最差。type 列是从左到右性能递减的,日常优化目标至少是 range 级别,避免 ALL。", + "source": null, + "related": [] + }, + { + "id": "fb-006", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "mysql", + "slow-query", + "explain" + ], + "question": "慢查询日志通过设置 ______ 参数(单位秒)开启,超过该阈值的查询会被记录。在 EXPLAIN 的 Extra 列中,______ 表示 MySQL 需要额外的排序操作,______ 表示使用了临时表,二者都可能消耗较多内存和 CPU,需要重点关注优化。", + "answer": [ + "slow_query_log", + "long_query_time", + "Using filesort", + "Using temporary" + ], + "answer_rule": "any", + "explanation": "慢查询日志通过 slow_query_log=ON 开启,long_query_time 设置阈值(默认 10 秒)。建议生产环境设为 1 秒甚至更低。EXPLAIN Extra 中:「Using filesort」表示 MySQL 无法利用索引完成排序,需要额外的排序算法(通常因为 ORDER BY 字段不在索引中);「Using temporary」表示使用了临时表(常见于 GROUP BY、DISTINCT、子查询等场景)。这两者通常意味着较大的内存和 CPU 开销,尤其在数据量大时性能影响显著,应通过优化索引和 SQL 来避免。", + "source": null, + "related": [] + }, + { + "id": "fb-007", + "type": "fill_blank", + "difficulty": 4, + "tags": [ + "mysql", + "lock" + ], + "question": "InnoDB 中,行锁基于索引实现,锁住的是索引记录而非物理行。当查询未命中索引时,InnoDB 会退化为______锁,锁定所有行,严重影响并发。间隙锁(Gap Lock)锁定的是索引记录之间的间隙,用于防止______插入,从而解决幻读问题。", + "answer": [ + "表锁", + "幻读", + "新行", + "其他事务的" + ], + "answer_rule": "any", + "explanation": "InnoDB 的行锁是通过给索引上的索引项加锁来实现的。如果 WHERE 条件没有走索引(如全表扫描),InnoDB 将锁住整个表(所有行),等效于表锁,严重降低并发性能。间隙锁(Gap Lock)锁住的是索引记录之间的「间隙」,以及第一条记录之前和最后一条记录之后的间隙,其目的是防止其他事务在间隙中插入新记录,从而避免幻读(Phantom Read)。临键锁(Next-Key Lock)= 行锁 + 间隙锁,是 InnoDB 在可重复读隔离级别下的默认锁类型。", + "source": null, + "related": [] + }, + { + "id": "fb-008", + "type": "fill_blank", + "difficulty": 4, + "tags": [ + "redis", + "cluster", + "slot" + ], + "question": "Redis Cluster 使用一致性哈希将数据分布在 16384 个 slot 中,每个 key 通过 CRC16 算法对 16384 取模确定所属 slot。当需要迁移 slot 时,使用 ______ 命令将 key-value 从源节点转移到目标节点,迁移过程中需要同时保持源和目标节点对该 slot 的访问。", + "answer": [ + "MIGRATE", + "migrate" + ], + "answer_rule": "any", + "explanation": "Redis Cluster 采用 16384 个虚拟节点(slot)来分配数据。每个 key 通过 CRC16(key) % 16384 确定 slot 编号,每个 master 节点负责一部分 slot。MIGRATE 命令用于在节点间迁移 key,它是一个原子操作:先在目标节点还原 key,成功后在源节点删除。迁移过程中,Redis Cluster 会通过 ASK/MOVED 重定向来保证客户端能正确路由请求。Redis Cluster 没有使用虚拟节点的概念,而是用固定的 16384 个 slot 做直接映射,每个节点负责一段连续的 slot 范围。", + "source": null, + "related": [] + }, + { + "id": "fb-009", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "redis", + "sentinel" + ], + "question": "Redis 哨兵(Sentinel)模式至少需要部署 ______ 个哨兵节点,以避免脑裂问题。哨兵通过类似 Raft 的协议选举 leader,当多数哨兵确认 master 不可达时触发故障转移。Sentinel 还负责 ______,即将新的 master 地址通知给所有客户端。", + "answer": [ + "3", + "客户端通知", + "通知客户端", + "配置发布", + "发布新的 master 地址" + ], + "answer_rule": "any", + "explanation": "Redis Sentinel 至少需要 3 个节点才能保证高可用,因为故障判定需要 quorum(多数派)同意。如果只有 2 个哨兵,任何一个挂掉都无法达成多数派,失去了容错能力。Sentinel 使用类似 Raft 的算法进行 leader 选举:当检测到 master 下线时,哨兵之间互相通信,通过投票选出 leader 来执行故障转移。故障转移完成后,Sentinel 通过「配置发布/订阅」机制(Pub/Sub)通知所有连接的客户端新的 master 地址,客户端会自动重连。", + "source": null, + "related": [] + }, + { + "id": "fb-010", + "type": "fill_blank", + "difficulty": 4, + "tags": [ + "redis", + "hybrid-persistence", + "active-defrag", + "lru" + ], + "question": "Redis 4.0 引入的混合持久化机制在 AOF rewrite 时,先以 RDB 格式写入当前全量数据,再追加 rewrite 期间的增量 AOF 命令,兼顾了 ______ 和 ______ 的优势。Redis 4.0 还引入了 ______ 功能,通过后台线程扫描并整理内存碎片,减少因频繁删改导致的内存浪费。当内存达到 maxmemory 时,Redis 根据 ______ 策略淘汰 key,常用策略包括 allkeys-lru、volatile-lru、volatile-lfu 等。", + "answer": [ + "RDB 恢复速度", + "AOF 数据完整性", + "恢复速度", + "数据安全", + "active-defrag", + "active-defragging", + "内存淘汰" + ], + "answer_rule": "any", + "explanation": "混合持久化(Hybrid Persistence)结合了 RDB 和 AOF 的优点:AOF rewrite 时先写 RDB 快照(恢复速度快),再追加增量 AOF 命令(保证数据完整性),大幅缩短了重启恢复时间。active-defrag 是 Redis 4.0 引入的主动内存碎片整理功能,通过后台线程对内存进行碎片扫描和重新分配,无需重启即可释放碎片内存,可通过 activedefrag yes 开启。Redis 的内存淘汰策略在内存达到 maxmemory 时触发:allkeys-lru 对所有 key 做 LRU 淘汰;volatile-lru 只对设了过期时间的 key 做 LRU;volatile-lfu 使用 LFU(最不经常使用)算法淘汰。LFU 比 LRU 更精准,能淘汰真正不活跃的 key。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/interview-prep/database-advanced/meta.json b/topics/interview-prep/database-advanced/meta.json new file mode 100644 index 0000000..04f0dee --- /dev/null +++ b/topics/interview-prep/database-advanced/meta.json @@ -0,0 +1,25 @@ +{ + "slug": "database-advanced", + "name": "数据库进阶", + "description": "B+树索引优化、EXPLAIN执行计划、Redis集群/哨兵、混合持久化、慢查询优化", + "tags": [], + "question_files": { + "single_choice": "single_choice.json", + "true_false": "true_false.json", + "fill_blank": "fill_blank.json", + "short_answer": "short_answer.json", + "code_reading": "code_reading.json" + }, + "stats": { + "total": 35, + "by_type": { + "single_choice": 10, + "true_false": 5, + "fill_blank": 10, + "short_answer": 5, + "code_reading": 5 + } + }, + "created": "2026-09-09", + "updated": "2026-09-09" +} \ No newline at end of file diff --git a/topics/interview-prep/database-advanced/short_answer.json b/topics/interview-prep/database-advanced/short_answer.json new file mode 100644 index 0000000..02e8c49 --- /dev/null +++ b/topics/interview-prep/database-advanced/short_answer.json @@ -0,0 +1,170 @@ +{ + "topic": "database-advanced", + "type": "short_answer", + "schema_version": "1.0.0", + "generated": "2026-09-09T00:00:00+08:00", + "questions": [ + { + "id": "sa-001", + "type": "short_answer", + "difficulty": 2, + "tags": [ + "mysql", + "bplus-tree", + "index", + "composite-index", + "covering-index" + ], + "question": "请解释 InnoDB 中聚簇索引与非聚簇索引的区别,并说明联合索引的最左前缀原则和覆盖索引的含义。针对表 orders(user_id, order_id, amount, status),若需要高效执行以下三种查询,分别应如何设计索引?\n1. SELECT * FROM orders WHERE user_id = 100 AND order_id = 'A001'\n2. SELECT user_id, order_id FROM orders WHERE user_id = 100\n3. SELECT amount FROM orders WHERE order_id = 'A001'", + "answer": "一、聚簇索引与非聚簇索引的区别:\n聚簇索引:InnoDB 中数据行按主键顺序物理存储,每个 InnoDB 表有且仅有一个聚簇索引。主键即聚簇索引;若无主键,选择第一个唯一非空索引;都没有则生成隐藏的 row_id 作为聚簇索引。叶子节点存储完整行数据。\n非聚簇索引(二级索引):叶子节点存储的是主键值而非完整行数据。通过非聚簇索引查询非索引列时,需要根据主键回表(回表查询),即通过主键再到聚簇索引中查找完整行。\n\n二、最左前缀原则:\n联合索引 (a, b, c) 按从左到右的顺序匹配。查询条件必须从最左列开始才能命中索引。可以跳过中间列吗?不能。例如 (a, b, c) 索引,WHERE a=1 AND c=3 只能用到 a 列,c 列无法使用索引。\n\n三、覆盖索引:\n当查询所需的所有列都包含在某个索引中时,无需回表,称为覆盖索引。EXPLAIN 中 Extra 列显示 Using index 表示使用了覆盖索引。\n\n针对三种查询的索引设计:\n1. WHERE user_id=100 AND order_id='A001':建联合索引 (user_id, order_id),两列均在索引中,可精确定位。\n2. SELECT user_id, order_id WHERE user_id=100:建联合索引 (user_id, order_id),user_id 用于过滤,user_id 和 order_id 均在索引中,构成覆盖索引,无需回表。\n3. SELECT amount WHERE order_id='A001':应单独建 order_id 索引,因为 amount 不在联合索引中(即使有联合索引也无法避免回表取 amount)。如果查询频繁,可考虑建覆盖索引 (order_id, amount) 来避免回表。", + "keywords": [ + "聚簇索引", + "非聚簇索引", + "二级索引", + "回表", + "主键", + "叶子节点", + "最左前缀", + "覆盖索引", + "Using index", + "联合索引" + ], + "scoring_rubric": "满分10分:1)正确描述聚簇索引与非聚簇索引的区别(含叶子节点存储内容差异)得3分;2)正确解释最左前缀原则得2分;3)正确解释覆盖索引得2分;4)三道查询的索引设计各1分。缺少回表概念扣1分,缺少覆盖索引与Using index关联扣1分。", + "explanation": "聚簇索引的核心特征是「数据即索引」——叶子节点直接存储完整行数据,因此一个表只能有一个聚簇索引。非聚簇索引(二级索引)的叶子节点只存主键值,查询非索引列时需要「回表」到聚簇索引再查一次。最左前缀原则是联合索引的匹配规则,本质是 B+ 树从左到右逐列排序的结构决定的。覆盖索引是一种优化手段:如果索引已经包含了查询需要的所有列,就完全不需要回表,大幅减少 I/O。这三者共同构成了 MySQL 索引优化的基础框架。", + "source": null, + "related": [] + }, + { + "id": "sa-002", + "type": "short_answer", + "difficulty": 3, + "tags": [ + "mysql", + "explain", + "slow-query" + ], + "question": "请解释 MySQL EXPLAIN 执行计划中 type 列和 Extra 列的常见值及其含义。针对以下慢查询,请分析 EXPLAIN 输出并给出优化建议:\n\nSELECT u.name, o.total FROM users u JOIN orders o ON u.id = o.user_id WHERE o.created_at > '2025-01-01' AND u.status = 1 GROUP BY u.name ORDER BY o.total DESC LIMIT 10;\n\n假设 EXPLAIN 输出为:type=ALL, rows=500000, Extra=Using temporary; Using filesort。", + "answer": "一、type 列常见值(从好到差):\n1. system:表只有一行,系统表,最优。\n2. const:通过主键或唯一索引单行查找,最多匹配一行,如 WHERE id=1。\n3. eq_ref:关联查询中,被驱动表通过主键或唯一索引进行等值匹配,每次关联只匹配一行。\n4. ref:通过普通索引进行等值匹配,可能返回多行。\n5. range:索引范围扫描,如 WHERE id > 10、WHERE id IN (1,2,3)。\n6. index:全索引扫描,遍历整个索引树(不回表)。\n7. ALL:全表扫描,最差,应尽量避免。\n\n二、Extra 列常见值:\n1. Using index:覆盖索引,无需回表,好。\n2. Using where:Server 层进行了额外过滤,不完全在存储引擎层完成。\n3. Using temporary:使用了临时表,常见于 GROUP BY / DISTINCT 等操作,需关注性能。\n4. Using filesort:需要额外排序操作,未利用索引排序,可能是性能瓶颈。\n5. Using join buffer:连接查询中被驱动表无可用索引,使用了 Join Buffer。\n6. Select tables optimized away:优化器直接从索引或统计信息中获取结果,无需扫描表。\n\n三、针对给定查询的优化建议:\n当前 type=ALL 表示全表扫描 orders 表(或 users 表),rows=500000 说明扫描了大量行。Extra 中 Using temporary 和 Using filesort 说明 GROUP BY 和 ORDER BY 都无法利用索引。\n\n优化措施:\n1. 为 orders 表建索引 (created_at, user_id, total)——created_at 用于 WHERE 过滤,user_id 用于 JOIN,total 用于覆盖排序和 SELECT。\n2. 为 users 表建索引 (status, id, name)——status 过滤,id 关联,name 覆盖 GROUP BY。\n3. 如分页不深,可考虑延迟关联优化:先查子查询获取 id 列表,再关联取完整数据。\n4. 使用 SHOW WARNINGS 查看优化器重写后的 SQL,确认查询是否被正确优化。", + "keywords": [ + "EXPLAIN", + "type", + "ALL", + "const", + "eq_ref", + "ref", + "range", + "Extra", + "Using temporary", + "Using filesort", + "Using index", + "慢查询", + "全表扫描", + "索引设计" + ], + "scoring_rubric": "满分10分:1)正确解释 type 列至少4个常见值(含全表扫描ALL)得3分;2)正确解释 Extra 列至少3个常见值(含Using temporary和Using filesort)得3分;3)针对给定查询给出至少2条合理优化建议得2分;4)能正确识别 type=ALL 和 Extra=Using temporary; Using filesort 的问题并关联到具体表得2分。", + "explanation": "EXPLAIN 是 MySQL 慢查询分析的核心工具。type 列反映了存储引擎查找数据的方式,ALL 是最差的全表扫描,const/eq_ref 是最优的单行查找。Extra 列揭示了额外执行信息:Using temporary 表示需要创建临时表来完成 GROUP BY,Using filesort 表示需要额外的排序操作,这两者在大数据量下是严重的性能隐患。在本例中,type=ALL + Using temporary + Using filesort 组合表明查询完全没有利用到索引,需要通过添加合适的联合索引来同时覆盖过滤、连接和排序需求。", + "source": null, + "related": [] + }, + { + "id": "sa-003", + "type": "short_answer", + "difficulty": 3, + "tags": [ + "redis", + "cluster", + "slot", + "consistent-hashing" + ], + "question": "Redis Cluster 使用 16384 个 slot 对数据进行分片。请回答:\n1. slot 的编号范围和分配方式是什么?\n2. 当客户端发送一个 GET key 命令时,Redis Cluster 如何确定该 key 应该由哪个节点处理?\n3. 在线扩容(增加一个新节点)的完整流程是什么?缩容时需要注意什么?\n4. 如果在扩容过程中,客户端访问了一个正在迁移中的 slot,会发生什么?", + "answer": "1. Slot 编号与分配:\n编号范围为 0-16383(共 16384 个 slot)。每个 Redis Cluster 节点负责一部分 slot。分配方式由 cluster.conf 中的 cluster-config-file 记录,默认均匀分配。例如 3 节点集群,节点 A 负责 0-5460,节点 B 负责 5461-10922,节点 C 负责 10923-16383。\n\n2. Key 到节点的路由:\n计算公式:slot = CRC16(key) % 16384。CRC16 是循环冗余校验算法。客户端(如 redis-cli)在发送命令前先计算 slot,然后根据集群元数据(slot 到节点的映射关系)将请求发送到对应的 master 节点。如果请求发到了错误节点,该节点会返回 MOVED 重定向响应,客户端更新路由缓存后重发。Redis 3.2+ 还支持 ASK 临时重定向(用于 slot 迁移中)。\n\n3. 在线扩容流程:\na) 启动新节点并加入集群:redis-cli --cluster add-node \nb) 分配 slot:redis-cli --cluster reshard ,指定要迁移的 slot 数量和目标节点。系统会自动选择源节点。\nc) 迁移过程:对每个要迁移的 slot,源节点执行 MIGRATE 命令将 slot 中的所有 key 迁移到目标节点。迁移期间源节点对迁移中的 key 的写操作会触发 ASK 重定向。\nd) 确认完成:redis-cli --cluster check 验证集群状态。\n\n缩容注意事项:\na) 先将要下线节点的 slot 迁移到其他节点(reshard)。\nb) 确认该节点上的所有 slot 已迁移完毕(cluster nodes 中该节点不再负责任何 slot)。\nc) 删除节点:redis-cli --cluster del-node。\nd) 注意:如果要下线的是 master 节点,需要先处理其 slave 节点(先 del slave,再 del master,或者先将 slave 绑定到其他 master)。\n\n4. 访问迁移中 slot 的行为:\nRedis Cluster 在 slot 迁移过程中支持 ASK 重定向。具体流程:\n- 客户端发送命令到源节点。\n- 源节点发现该 slot 正在迁移到目标节点(IMPORTING 状态),如果 key 还在源节点则正常处理;如果 key 已迁走,返回 ASK 重定向到目标节点。\n- 客户端收到 ASK 后,向目标节点发送 ASKING + 原命令。注意 ASK 是单次重定向,不会更新客户端的 slot 路由缓存(与 MOVED 不同)。\n- 迁移完成后,源节点发送 MOVED,客户端永久更新路由。", + "keywords": [ + "16384", + "CRC16", + "slot", + "MOVED", + "ASK", + "reshard", + "MIGRATE", + "cluster", + "分片", + "扩容", + "缩容", + "重定向" + ], + "scoring_rubric": "满分10分:1)正确描述 slot 编号范围和 CRC16 路由机制得3分;2)正确解释 MOVED 和 ASK 重定向的区别得2分;3)正确描述扩容完整流程(add-node + reshard + MIGRATE)得3分;4)正确说明缩容注意事项(先迁移 slot 再删除节点)得2分。缺少 CRC16 公式扣1分,混淆 MOVED 和 ASK 扣1分。", + "explanation": "Redis Cluster 使用哈希槽(hash slot)实现数据分片,是典型的分布式哈希方案。16384 这个数字的选择是权衡了节点数量上限(约 1000 个)和每个节点需要维护的 slot 位图大小(16384/8=2048 字节)后的结果。CRC16%16384 保证了数据在节点间的均匀分布。MOVED 和 ASK 的设计体现了分布式系统中「强一致路由」与「临时过渡路由」的分离,ASK 仅在迁移期间使用且不更新客户端缓存,避免了全局路由信息的不一致。", + "source": null, + "related": [] + }, + { + "id": "sa-004", + "type": "short_answer", + "difficulty": 4, + "tags": [ + "mysql", + "lock", + "deadlock" + ], + "question": "请详细解释 MySQL InnoDB 中以下三种锁的区别和应用场景:\n1. 行锁(Record Lock)\n2. 间隙锁(Gap Lock)\n3. 临键锁(Next-Key Lock)\n\n并回答以下问题:\n- 在可重复读(RR)隔离级别下,以下 SQL 会产生什么类型的锁?\n SELECT * FROM accounts WHERE balance BETWEEN 1000 AND 5000 FOR UPDATE;\n (假设 balance 列有索引,值分布为 500, 2000, 8000)\n- 什么是死锁?MySQL 如何检测死锁?如何预防死锁?", + "answer": "一、三种锁的区别:\n1. 行锁(Record Lock):锁定索引记录本身。只锁住该行,其他事务可以修改同一范围的其他行。是最细粒度的锁。\n\n2. 间隙锁(Gap Lock):锁定索引记录之间的间隙(即两个索引值之间的范围),不锁定记录本身。目的是防止其他事务在间隙中插入新记录,用于解决幻读问题。例如索引值为 5 和 10,则间隙锁可能锁住 (5, 10) 这个范围。\n\n3. 临键锁(Next-Key Lock):行锁 + 间隙锁的组合,锁定一个索引记录及其前面的间隙。即锁定 [gap, record]。InnoDB 在 RR 隔离级别下的默认加锁方式。例如索引值为 5, 10, 15,临键锁可能锁住 (5, 10] 这个区间。\n\n二、针对给定 SQL 的锁分析:\nSELECT * FROM accounts WHERE balance BETWEEN 1000 AND 5000 FOR UPDATE;\n索引值分布为 500, 2000, 8000。\n\n在 RR 隔离级别下:\n- balance 在 1000-5000 范围内的索引值只有 2000,所以会对 balance=2000 这行加行锁。\n- 由于 BETWEEN 1000 AND 5000 的范围,还会在两侧间隙加间隙锁:(500, 1000) 和 (2000, 5000)——实际是左闭右开的区间。\n- 综合起来:临键锁覆盖 (500, 2000] + (2000, 5000),即防止在此范围内插入新值(如 balance=3000 的记录),同时锁住已存在的 2000 这行。\n\n三、死锁相关:\n1. 死锁定义:两个或多个事务互相持有对方需要的锁,形成循环等待,永远无法继续执行。\n\n2. MySQL 检测机制:InnoDB 使用「等待图」(wait-for graph)检测死锁。维护一个事务依赖关系图,如果图中出现环路,则存在死锁。InnoDB 有一个 innodb_deadlock_detect 参数控制是否开启检测(默认开启)。还可以通过 innodb_lock_wait_timeout 设置锁等待超时。\n\n3. 死锁处理:检测到死锁后,InnoDB 选择一个「代价最小」的事务进行回滚(通常是更新最少行的事务),并向该事务返回错误 ERROR 1213 (40001): Deadlock found。\n\n4. 死锁预防措施:\na) 保持事务尽量短小,减少持锁时间。\nb) 事务中的 SQL 按固定顺序访问表和行,避免交叉加锁。\nc) 合理设计索引,使 SQL 能精确锁定需要的行,避免大范围加锁(如使用覆盖索引减少锁范围)。\nd) 在业务允许的情况下降低隔离级别为 READ COMMITTED(RC),RC 下没有间隙锁,大大降低死锁概率。\ne) 使用 SELECT ... FOR UPDATE NOWAIT 或 SKIP LOCKED 避免锁等待。", + "keywords": [ + "行锁", + "Record Lock", + "间隙锁", + "Gap Lock", + "临键锁", + "Next-Key Lock", + "死锁", + "等待图", + "innodb_deadlock_detect", + "幻读", + "RR隔离级别", + "FOR UPDATE" + ], + "scoring_rubric": "满分10分:1)正确区分三种锁的定义和区别得3分;2)正确分析给定 SQL 产生的锁类型及范围得3分;3)正确解释死锁定义和检测机制得2分;4)给出至少3条有效的死锁预防措施得2分。间隙锁范围分析错误扣1分,未提及 RC 隔离级别降低死锁扣1分。", + "explanation": "InnoDB 的锁机制围绕解决幻读问题而设计。在 RR 隔离级别下,临键锁(Next-Key Lock)是默认加锁方式,它结合了行锁和间隙锁的优点:既锁住已有记录防止被修改,又锁住间隙防止新记录插入。理解锁的范围对于死锁预防至关重要——很多死锁正是因为事务锁定了过大的范围(如范围查询无精确索引导致锁全表间隙),使两个事务的锁范围产生交叉。降低隔离级别到 RC 是一种激进但有效的策略,因为 RC 下只有行锁没有间隙锁,但代价是可能出现不可重复读。", + "source": null, + "related": [] + }, + { + "id": "sa-005", + "type": "short_answer", + "difficulty": 4, + "tags": [ + "redis", + "sentinel", + "hybrid-persistence", + "slow-query" + ], + "question": "请分别回答以下三部分内容:\n\n一、Redis 哨兵(Sentinel)模式:\n1. 哨兵节点之间如何选举 leader?采用什么算法?\n2. 故障转移(failover)的完整流程是什么?\n\n二、Redis 混合持久化(Hybrid Persistence):\n1. RDB 和 AOF 各自的优缺点是什么?\n2. 混合持久化是如何结合 RDB 和 AOF 的?AOF rewrite 的具体流程是什么?\n3. 故障恢复时,混合持久化的恢复流程是怎样的?\n\n三、Redis 内存优化:\n1. 如何排查 Redis 中的大 key 问题?大 key 有哪些危害?\n2. Redis 的内存淘汰策略有哪些?各自适用什么场景?", + "answer": "一、Redis 哨兵:\n1. Leader 选举采用 Raft 算法:\n- 每个 Sentinel 节点每秒向其他节点发送 PING。\n- 当某个 Sentinel 发现 master 在 down-after-milliseconds 时间内无响应,标记为 SDOWN(主观下线)。\n- 当 quorum(法定人数)个 Sentinel 都标记为 SDOWN 时,master 被标记为 ODOWN(客观下线)。\n- Sentinel 发起 leader 选举:每个 Sentinel 向其他 Sentinel 发送 Sentinel is-master-down-by-addr 命令请求投票。\n- 每个 Sentinel 在一个配置纪元(epoch)中只能投一票,先到先得。\n- 获得 quorum+1 票(即超过半数)的 Sentinel 成为 leader,执行故障转移。\n\n2. 故障转移流程:\na) Leader Sentinel 从 slave 列表中筛选新 master:\n - 排除已下线的、断线时间过长的(超过 down-after-milliseconds * 10)、slave-priority=0 的节点。\n - 优先选择 offset 最大(数据最新)的 slave。\nb) 对选中的 slave 执行 SLAVEOF NO ONE,使其成为 master。\nc) 向其他 slave 发送 SLAVEOF ,使它们复制新 master。\nd) 更新 sentinel.conf 中的 master 地址,通知客户端新的 master。\n\n二、Redis 混合持久化:\n1. RDB 优缺点:\n优点:文件紧凑,恢复速度快,适合备份和灾难恢复。\n缺点:两次快照之间的数据可能丢失(取决于 RDB 间隔);fork 子进程时可能占用较多内存。\n\nAOF 优缺点:\n优点:数据安全性高(everysec 策略最多丢1秒数据),可读性强。\n缺点:文件体积大,恢复速度慢(需重放所有写命令)。\n\n2. 混合持久化(Redis 4.0+,aof-use-rdb-preamble yes):\n在 AOF rewrite 时,不再是纯命令重放,而是:\na) 先以 RDB 格式将当前全量数据写入 AOF 文件头部(RDB preamble)。\nb) rewrite 期间的增量写命令追加在 RDB 数据之后(AOF 格式)。\nc) 最终 AOF 文件 = RDB 格式的全量数据 + AOF 格式的增量数据。\n\nAOF rewrite 流程:\n1) Redis 创建子进程执行 rewrite(类似 RDB 的 fork)。\n2) 子进程遍历内存数据,生成新的 AOF 文件(混合模式下先写 RDB 格式全量数据,再写增量命令)。\n3) rewrite 期间的写命令同时写入旧 AOF 文件的缓冲区和新 AOF 文件尾部。\n4) 子进程完成后,主进程将缓冲区中的增量数据追加到新 AOF 文件。\n5) 原子替换旧 AOF 文件为新 AOF 文件。\n\n3. 故障恢复流程:\na) 检查是否开启混合持久化(aof-use-rdb-preamble)。\nb) 如果开启:先以 RDB 方式恢复全量数据(快速),然后重放 RDB 之后的 AOF 命令恢复增量数据。\nc) 如果未开启纯 AOF 模式:重放所有 AOF 命令(较慢)。\nd) 如果只有 RDB:直接加载 RDB 文件(最快,但可能丢失最近数据)。\ne) 恢复优先级:AOF > RDB(如果两者都存在)。\n\n三、Redis 内存优化:\n1. 大 key 排查方法:\na) redis-cli --bigkeys:扫描集群,找出每个数据类型中最大的 key。\nb) redis-cli --memkeys:按内存使用量排序。\nc) SCAN + MEMORY USAGE:遍历所有 key,用 MEMORY USAGE 命令逐个检查。\nd) 使用 RDB 工具(如 rdb)离线分析 dump.rdb 文件。\ne) 在 Redis 6.0+ 使用 OBJECT HELP 和 DEBUG 检查。\n\n大 key 危害:\na) 操作延迟高:DEL 大 key 可能阻塞主线程(Redis 4.0+ 已改为异步删除)。\nb) 网络带宽占用:读写大 key 消耗大量网络资源。\nc) 内存不均:在 Cluster 中可能导致某个节点内存远大于其他节点。\nd) 持久化受影响:fork 时大 key 增加写时复制(COW)的内存开销。\ne) 过期删除开销:大 key 过期时可能造成短暂的性能抖动。\n\n2. 内存淘汰策略(8种):\na) noeviction(默认):内存满时拒绝写入,返回错误。适合不允许数据丢失的场景。\nb) allkeys-lru:所有 key 中淘汰最近最少使用的。适合热点数据明显的通用场景。\nc) volatile-lru:只在设置了过期时间的 key 中淘汰 LRU。适合有明确过期策略的缓存。\nd) allkeys-lfu:所有 key 中淘汰最不经常使用的(Redis 4.0+)。适合访问频率差异大的场景。\ne) volatile-lfu:只在有过期时间的 key 中淘汰 LFU。\nf) allkeys-random:所有 key 中随机淘汰。适合各 key 访问概率相近的场景。\ng) volatile-random:只在有过期时间的 key 中随机淘汰。\nh) volatile-ttl:只在有过期时间的 key 中淘汰 TTL 最小(即将过期)的。适合按时效性管理数据。\n\n选择建议:缓存场景首选 allkeys-lru;有明确过期需求用 volatile-lru 或 volatile-ttl;对数据准确性要求高用 noeviction。", + "keywords": [ + "Sentinel", + "Raft", + "leader选举", + "SDOWN", + "ODOWN", + "quorum", + "failover", + "混合持久化", + "RDB", + "AOF", + "rewrite", + "fork", + "aof-use-rdb-preamble", + "大key", + "bigkeys", + "MEMORY USAGE", + "noeviction", + "allkeys-lru", + "allkeys-lfu", + "volatile-ttl" + ], + "scoring_rubric": "满分15分:哨兵部分5分(leader选举算法2分 + 故障转移流程3分);混合持久化部分5分(RDB/AOF优缺点1分 + 混合原理2分 + 恢复流程2分);内存优化部分5分(大key排查2分 + 危害1分 + 淘汰策略2分)。每个部分答出核心要点即可得分,细节不完整酌情扣分。", + "explanation": "Redis 的高可用方案经历了主从→哨兵→Cluster 的演进。哨兵通过 Raft 算法选举 leader 来协调故障转移,避免了脑裂问题。混合持久化是 Redis 4.0 的重要改进,它结合了 RDB 的快速恢复和 AOF 的数据安全性,通过「RDB头 + AOF尾」的文件格式,使故障恢复既快又全。内存优化方面,大 key 是 Redis 运维中最常见的性能杀手之一,而内存淘汰策略的选择需要根据业务场景的数据重要性、访问模式和时效性来综合判断。这三部分共同构成了 Redis 生产环境运维的核心知识体系。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/interview-prep/database-advanced/single_choice.json b/topics/interview-prep/database-advanced/single_choice.json new file mode 100644 index 0000000..8b44e62 --- /dev/null +++ b/topics/interview-prep/database-advanced/single_choice.json @@ -0,0 +1,217 @@ +{ + "topic": "database-advanced", + "type": "single_choice", + "schema_version": "1.0.0", + "generated": "2026-09-09T00:00:00+08:00", + "questions": [ + { + "id": "sc-001", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "mysql", + "bplus-tree", + "index" + ], + "question": "在 InnoDB 中,关于聚簇索引和非聚簇索引(二级索引)的区别,以下说法正确的是?", + "options": { + "A": "聚簇索引的叶子节点存储的是完整行数据,非聚簇索引的叶子节点存储的是主键值", + "B": "每个表可以有多个聚簇索引,但只能有一个非聚簇索引", + "C": "聚簇索引的查询效率一定比非聚簇索引高,因此应尽量为所有列都建立聚簇索引", + "D": "非聚簇索引查找数据时不需要回表,因为叶子节点直接指向磁盘上的行" + }, + "answer": "A", + "explanation": "InnoDB 的聚簇索引(通常即主键索引)的叶子节点直接存储完整行数据,而非聚簇索引(二级索引)的叶子节点存储的是主键值。通过二级索引查找数据时,需要先拿到主键值,再回到聚簇索引中查找完整行,这个过程称为「回表」。每个 InnoDB 表只能有一个聚簇索引(数据物理上按聚簇索引排序存储),但可以有多个非聚簇索引。索引过多会增加写入开销和存储空间,不应盲目建立。", + "source": null, + "related": [] + }, + { + "id": "sc-002", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "mysql", + "bplus-tree", + "composite-index" + ], + "question": "有一张表建立了联合索引 INDEX idx_abc(a, b, c),以下哪个查询能有效利用该索引?", + "options": { + "A": "SELECT * FROM t WHERE b = 1 AND c = 2", + "B": "SELECT * FROM t WHERE c = 2 AND a = 1", + "C": "SELECT * FROM t WHERE a = 1 AND c = 2", + "D": "SELECT * FROM t WHERE a = 1 AND b > 5 AND c = 2" + }, + "answer": "B", + "explanation": "联合索引遵循最左前缀原则。MySQL 优化器会自动调整 WHERE 条件的顺序,所以选项 B(c=2 AND a=1)等价于 a=1 AND c=2,可以使用索引的 a 列。但注意 c=2 无法使用索引(中间跳过了 b)。选项 A 完全跳过了最左列 a,无法使用该索引。选项 C 使用了 a 但跳过了 b 直接用 c,只能用到 a 列。选项 D 中 b 是范围查询(b>5),根据最左前缀原则,范围查询后的列 c 无法使用索引,但 a 和 b 部分可以使用。不过选项 B 能让优化器识别出 a=1 可用索引,是本题最明确能利用索引的选项。", + "source": null, + "related": [] + }, + { + "id": "sc-003", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "mysql", + "bplus-tree", + "index", + "covering-index" + ], + "question": "关于覆盖索引(Covering Index),以下描述正确的是?", + "options": { + "A": "覆盖索引是指索引包含了查询所需的所有列,从而避免回表操作", + "B": "覆盖索引要求索引列必须是主键,否则无法覆盖", + "C": "使用覆盖索引时,EXPLAIN 的 Extra 列会显示 Using filesort", + "D": "覆盖索引只适用于等值查询,范围查询无法使用覆盖索引" + }, + "answer": "A", + "explanation": "覆盖索引是指查询所需的列都包含在索引中,无需回表读取聚簇索引中的完整行数据。EXPLAIN 中 Extra 列显示 Using index 表示使用了覆盖索引。覆盖索引不限于主键索引,任何二级索引只要包含了查询所需的列即可。范围查询同样可以利用覆盖索引,只要索引包含了所需列,例如 INDEX(name, age) 可以覆盖 SELECT name, age FROM t WHERE name > 'a'。Using filesort 表示需要额外排序,与覆盖索引无关。", + "source": null, + "related": [] + }, + { + "id": "sc-004", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "mysql", + "explain" + ], + "question": "以下 EXPLAIN 输出中,type 列的值从最优到最差的正确排列顺序是?", + "options": { + "A": "ALL > index > range > ref > const", + "B": "const > ref > range > index > ALL", + "C": "const > range > ref > index > ALL", + "D": "range > const > ref > ALL > index" + }, + "answer": "B", + "explanation": "EXPLAIN 的 type 列表示 MySQL 访问表的方式,从最优到最差的顺序为:system > const > eq_ref > ref > range > index > ALL。const 表示通过主键或唯一索引精确匹配一行;ref 表示非唯一索引等值匹配;range 表示索引范围扫描;index 表示全索引扫描(遍历索引树);ALL 表示全表扫描(最差)。选项 B 正确地将 const > ref > range > index > ALL 从最优到最差排列。", + "source": null, + "related": [] + }, + { + "id": "sc-005", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "mysql", + "explain" + ], + "question": "EXPLAIN 的 Extra 列出现 Using temporary 和 Using filesort,通常意味着查询需要优化。以下哪种查询最容易同时出现这两个警告?", + "options": { + "A": "SELECT * FROM t WHERE id = 1", + "B": "SELECT DISTINCT col1 FROM t ORDER BY col2 LIMIT 10", + "C": "SELECT * FROM t FORCE INDEX(PRIMARY) WHERE id BETWEEN 1 AND 100", + "D": "SELECT count(*) FROM t WHERE col1 = 'value'" + }, + "answer": "B", + "explanation": "选项 B 的查询同时涉及 DISTINCT(去重)和 ORDER BY(排序),且 col1 和 col2 不是同一个列,无法通过索引同时满足。MySQL 需要创建临时表来处理 DISTINCT,并且需要 filesort 来完成 ORDER BY,因此同时出现 Using temporary 和 Using filesort。选项 A 只是主键等值查询,type 为 const,无需临时表或排序。选项 C 使用 FORCE INDEX 做范围扫描,通常也不会触发临时表。选项 D 是简单的 WHERE 条件聚合,count(*) 不需要排序或去重。", + "source": null, + "related": [] + }, + { + "id": "sc-006", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "mysql", + "slow-query" + ], + "question": "关于 MySQL 慢查询日志,以下配置和说法正确的是?", + "options": { + "A": "slow_query_log=ON 开启慢查询日志后,所有查询都会被记录到日志中", + "B": "long_query_time 默认值为 10 秒,表示执行时间超过 10 秒的 SQL 才会被记录", + "C": "慢查询日志只能记录 SELECT 语句,不包括 INSERT/UPDATE/DELETE", + "D": "开启慢查询日志会对数据库性能产生显著影响,生产环境不应开启" + }, + "answer": "B", + "explanation": "MySQL 慢查询日志(slow_query_log)默认 long_query_time 为 10 秒,只有执行时间超过该阈值的 SQL 才会被记录。慢查询日志记录所有类型的语句(SELECT、INSERT、UPDATE、DELETE 等),只要执行时间超过阈值。默认情况下只有管理员可以查看慢查询日志,不会记录所有查询,因此对性能影响极小,生产环境强烈建议开启以便排查性能问题。也可以设置 log_queries_not_using_indexes=ON 记录未使用索引的查询。", + "source": null, + "related": [] + }, + { + "id": "sc-007", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "mysql", + "lock", + "next-key-lock", + "gap-lock" + ], + "question": "在 InnoDB 的默认隔离级别(REPEATABLE READ)下,执行以下语句时,关于锁的行为描述正确的是?\n\nDELETE FROM orders WHERE order_id = 100;\n假设 order_id 是主键列。", + "options": { + "A": "会加行锁,锁定 order_id = 100 的那一行记录", + "B": "会加临键锁(Next-Key Lock),锁定 order_id = 100 前面的间隙", + "C": "会加间隙锁(Gap Lock),锁定 order_id = 100 周围的范围", + "D": "REPEATABLE READ 级别下不会加任何锁" + }, + "answer": "A", + "explanation": "当使用主键进行精确等值匹配(WHERE order_id = 100)时,InnoDB 只需要加一个行锁(Record Lock)锁定该行即可,不需要间隙锁或临键锁。间隙锁和临键锁主要用于防止幻读,在非唯一索引的范围查询或主键的范围查询时才会出现。等值查询命中已存在的记录时,锁的粒度最小,只锁定匹配的那一行。DELETE 语句在 REPEATABLE READ 级别下会获取排他锁(X Lock),即该行在事务提交前不能被其他事务修改或读取(当前读)。", + "source": null, + "related": [] + }, + { + "id": "sc-008", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "redis", + "cluster", + "slot", + "consistent-hashing" + ], + "question": "Redis Cluster 使用 16384 个 slot 分片来分布数据,关于 slot 的分配和路由机制,以下说法正确的是?", + "options": { + "A": "客户端发送任意 key 的命令时,Redis Cluster 会自动将 key 路由到任意一个节点处理", + "B": "使用 CRC16 对 key 计算哈希值后对 16384 取模来确定 key 所属的 slot", + "C": "每个 Redis 节点平均分配约 16384 个 slot,节点数固定后无法调整", + "D": "16384 个 slot 必须全部分配完毕才能正常提供服务,任何 slot 未分配都会导致集群不可用" + }, + "answer": "B", + "explanation": "Redis Cluster 使用 CRC16(key) % 16384 来确定每个 key 所属的 slot。每个节点负责一部分 slot,客户端发送命令时,如果 key 不在当前节点,会收到 MOVED 重定向响应。slot 的分配是动态的,可以通过 reshard 操作在节点间迁移 slot。只要至少有 6470 个 slot 被分配(覆盖所有 slot 的一半以上 + 每个 master 至少一个),集群就可以正常工作,但通常建议 100% 覆盖。CRC16 是一种高效的哈希算法,16384 个 slot 使用 2KB 的位图在节点间通信,这是选择 16384 而非更大数字的原因之一。", + "source": null, + "related": [] + }, + { + "id": "sc-009", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "redis", + "sentinel" + ], + "question": "关于 Redis Sentinel(哨兵)的故障转移流程,以下描述正确的是?", + "options": { + "A": "当 master 被标记为主观下线(SDOWN)后,Sentinel 会立即开始故障转移", + "B": "Sentinel 通过 Raft 协议选举 Leader,由 Leader 执行故障转移操作", + "C": "故障转移时,Sentinel 会优先选择 slave 的 offset 最大的节点作为新 master", + "D": "故障转移完成后,Sentinel 不会通知客户端新的 master 地址,客户端需要自行探测" + }, + "answer": "B", + "explanation": "Redis Sentinel 的故障转移流程:首先,单个 Sentinel 发现 master 不响应,标记为 SDOWN(主观下线);当足够多的 Sentinel(quorum 数量)都认为 master SDOWN 后,标记为 ODOWN(客观下线);然后通过类似 Raft 的选举机制选出一个 Leader Sentinel;由 Leader 执行故障转移:从 slave 中选择合适的节点提升为新 master(优先级 > offset > runid),通知其他 slave 复制新 master,通知客户端新的 master 地址。注意不是 offset 最大就优先——还需要先看 slave-priority,只有 priority 相同时才比较 offset。", + "source": null, + "related": [] + }, + { + "id": "sc-010", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "redis", + "hybrid-persistence", + "lru" + ], + "question": "关于 Redis 的持久化和内存管理,以下说法正确的是?", + "options": { + "A": "Redis 混合持久化(hybrid persistence)是在 RDB 快照的基础上追加 AOF 日志,重启时先加载 RDB 再重放增量 AOF,兼顾恢复速度和数据完整性", + "B": "Redis 混合持久化是将 RDB 和 AOF 数据同时写入同一个文件,恢复时随机读取两者的数据进行合并", + "C": "Redis 的 allkeys-lru 策略只会在键空间不足时淘汰设置了过期时间的 key", + "D": "Redis 开启内存碎片整理(activedefrag)会导致主线程完全阻塞,建议生产环境关闭" + }, + "answer": "A", + "explanation": "Redis 混合持久化(Redis 4.0+)在 AOF 重写时,将当前 RDB 快照数据写入 AOF 文件头部,重写后的增量命令追加在 RDB 数据之后。重启时先加载 RDB 部分(快速恢复大部分数据),再重放后面的 AOF 增量命令(保证数据完整性)。这结合了 RDB 恢复速度快和 AOF 数据丢失少的优点。allkeys-lru 策略会在所有 key 中(不论是否设置了过期时间)淘汰最近最少使用的 key。Redis 4.0+ 的 activedefrag 在后台线程执行碎片整理,不会完全阻塞主线程,生产环境可以开启。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/interview-prep/database-advanced/true_false.json b/topics/interview-prep/database-advanced/true_false.json new file mode 100644 index 0000000..e4ace9b --- /dev/null +++ b/topics/interview-prep/database-advanced/true_false.json @@ -0,0 +1,80 @@ +{ + "topic": "database-advanced", + "type": "true_false", + "schema_version": "1.0.0", + "generated": "2026-09-09T00:00:00+08:00", + "questions": [ + { + "id": "tf-001", + "type": "true_false", + "difficulty": 3, + "tags": [ + "mysql", + "bplus-tree", + "composite-index" + ], + "question": "在MySQL中,对于联合索引(a, b, c),当查询条件为 WHERE b = 1 AND c = 2 时,优化器可以自动调整条件顺序以匹配索引最左前缀,从而使用该索引。", + "answer": false, + "explanation": "联合索引(a, b, c)要求查询条件必须从最左列a开始才能命中索引。优化器虽然可以重排WHERE子句中条件的执行顺序(将 b=1 AND c=2 调整为 c=2 AND b=1),但无论怎么调整,条件中始终缺少最左列a,因此该联合索引完全无法被使用。最左前缀原则是B+树联合索引的物理存储结构决定的——索引按照(a, b, c)的顺序排序,没有a的值就无法在B+树上进行有效的范围定位。", + "source": null, + "related": [] + }, + { + "id": "tf-002", + "type": "true_false", + "difficulty": 2, + "tags": [ + "mysql", + "explain" + ], + "question": "在MySQL EXPLAIN输出中,type列的值为'index'表示全表扫描,而'ALL'表示使用了索引扫描。", + "answer": false, + "explanation": "这道题将两者搞反了。type='ALL'表示全表扫描(Full Table Scan),是最差的访问方式,需要逐行扫描聚簇索引中的所有记录。type='index'表示全索引扫描(Full Index Scan),虽然也需要扫描整个索引树,但至少使用了二级索引,在覆盖索引场景下甚至不需要回表,因此性能通常优于ALL。访问效率从好到差的典型顺序为:system > const > eq_ref > ref > range > index > ALL。", + "source": null, + "related": [] + }, + { + "id": "tf-003", + "type": "true_false", + "difficulty": 3, + "tags": [ + "mysql", + "slow-query", + "index" + ], + "question": "开启MySQL慢查询日志后,所有执行时间超过long_query_time阈值的SQL语句都会被完整记录到慢查询日志中。", + "answer": false, + "explanation": "超过long_query_time阈值的SQL并不一定会被记录,还受到min_examined_row_limit参数的影响——只有实际扫描行数超过该值的查询才会被记录。默认情况下min_examined_row_limit=0,此时所有超过阈值的查询都会被记录;但如果设置了非零值,那些虽然执行时间超标但扫描行数很少的SQL会被过滤掉。此外,慢查询日志的开启需要先设置slow_query_log=ON,且记录的是执行时间(而非锁等待时间),锁等待时间可通过log_slow_admin_statements等参数单独控制。", + "source": null, + "related": [] + }, + { + "id": "tf-004", + "type": "true_false", + "difficulty": 4, + "tags": [ + "mysql", + "lock" + ], + "question": "InnoDB的间隙锁(Gap Lock)和临键锁(Next-Key Lock)在读已提交(READ COMMITTED)隔离级别下仍然生效,用于防止幻读。", + "answer": false, + "explanation": "间隙锁和临键锁只在可重复读(REPEATABLE READ)及以上隔离级别下生效。在读已提交(READ COMMITTED)级别下,InnoDB仅使用记录锁(Record Lock)锁定索引记录本身,不会对索引记录之间的间隙加锁。这意味着RC级别下无法防止幻读——另一个事务可以在间隙中插入新记录。这是MySQL默认选择RR作为隔离级别的重要原因之一:通过间隙锁+临键锁在RR级别下实现幻读防护。注意意向锁(Intention Lock)和元数据锁(MDL)在所有隔离级别下都存在,它们与间隙锁是不同层面的锁机制。", + "source": null, + "related": [] + }, + { + "id": "tf-005", + "type": "true_false", + "difficulty": 3, + "tags": [ + "redis", + "cluster" + ], + "question": "在Redis Cluster中,当某个master节点负责的slot持续无法被访问时,该master的slave会自动发起投票选举并完成故障转移,客户端通过MOVED或ASK重定向自动感知拓扑变化。", + "answer": true, + "explanation": "Redis Cluster的故障转移机制如下:1) 每个master节点定期向其他节点发送PING,当某个master被标记为PFAIL(疑似下线)后,集群通过Gossip协议传播该状态;2) 当超过半数的master节点都将该节点标记为FAIL时,其slave会发起选举,通过类似Raft的投票机制竞争晋升为新master;3) 获得多数投票的slave被提升为master,接管原master负责的slot,并向集群广播配置更新。客户端收到MOVED响应后会更新本地路由表,后续请求直接发往新master,整个过程对应用层基本透明。值得注意的是,Redis Cluster默认需要至少3个master节点且每个master至少1个slave才能保证高可用。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/interview-prep/distributed-microservice/code_reading.json b/topics/interview-prep/distributed-microservice/code_reading.json new file mode 100644 index 0000000..b19c14d --- /dev/null +++ b/topics/interview-prep/distributed-microservice/code_reading.json @@ -0,0 +1,296 @@ +{ + "topic": "distributed-microservice", + "type": "code_reading", + "schema_version": "1.0.0", + "generated": "2026-09-09T00:00:00+08:00", + "questions": [ + { + "id": "cr-001", + "type": "code_reading", + "difficulty": 3, + "tags": [ + "microservice", + "distributed-lock" + ], + "question": "阅读以下基于 Redis 的分布式锁实现代码,回答子问题。", + "explanation": "本题考查 Redis 分布式锁的核心原理,包括 SET NX EX 原子操作、过期续期、以及 Lua 脚本保证解锁的原子性。", + "code": "package lock\n\nimport (\n \"context\"\n \"time\"\n \"github.com/go-redis/redis/v8\"\n)\ntype RedisLock struct {\n client *redis.Client\n key string\n value string\n ttl time.Duration\n}\nfunc (l *RedisLock) Lock(ctx context.Context) (bool, error) {\n return l.client.SetNX(ctx, l.key, l.value, l.ttl).Result()\n}\nvar unlockScript = redis.NewScript(`\n if redis.call(\"get\", KEYS[1]) == ARGV[1] then\n return redis.call(\"del\", KEYS[1])\n end\n return 0\n`)\nfunc (l *RedisLock) Unlock(ctx context.Context) error {\n _, err := unlockScript.Run(ctx, l.client, []string{l.key}, l.value).Result()\n return err\n}\nfunc (l *RedisLock) AutoRenew(ctx context.Context, stopCh <-chan struct{}) {\n ticker := time.NewTicker(l.ttl / 3)\n defer ticker.Stop()\n for {\n select {\n case <-ticker.C:\n l.client.Expire(ctx, l.key, l.ttl)\n case <-stopCh:\n return\n }\n }\n}", + "language": "go", + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "代码中解锁操作使用 Lua 脚本而不是先 GET 再 DEL 的两步操作,最主要的原因是什么?", + "options": { + "A": "减少网络往返次数,提升性能", + "B": "保证 GET 判断和 DEL 删除的原子性,防止误删其他线程的锁", + "C": "Lua 脚本可以绕过 Redis 的单线程限制", + "D": "Redis 不支持 GET 和 DEL 的组合命令" + }, + "answer": "B", + "explanation": "先 GET 再 DEL 的两步操作不是原子的:在 GET 之后、DEL 之前,锁可能刚好过期被其他客户端获取,此时当前客户端会误删别人的锁。Lua 脚本在 Redis 单线程中原子执行,确保 GET 和 DEL 在同一事务中完成,避免误删。" + }, + { + "index": 2, + "type": "short_answer", + "question": "代码中 AutoRenew 方法使用 ttl/3 作为续期间隔,而非在锁过期后才续期。请分析这样设计的两个主要目的。", + "answer": "1) 防止业务未完成但锁已过期导致并发冲突;2) 留有余量避免续期过于频繁造成性能浪费", + "keywords": [ + "防止锁过期", + "业务未完成", + "并发冲突", + "余量", + "性能" + ], + "explanation": "使用 ttl/3 作为续期间隔有两个目的:第一,在锁自然过期前多次续期,确保业务执行期间锁始终有效,防止锁提前过期导致其他客户端获取锁产生并发冲突;第二,选择 ttl/3 而非更短的间隔,是在续期及时性和 Redis 请求频率之间的平衡,避免过度消耗 Redis 资源。" + }, + { + "index": 3, + "type": "single_choice", + "question": "value 字段使用 UUID 作为唯一标识的主要作用是什么?", + "options": { + "A": "用于区分不同的锁实例", + "B": "作为锁的密钥,防止外部猜测", + "C": "确保只有持有该 value 的客户端才能解锁,实现锁的安全释放", + "D": "用于在 Redis 中存储锁的创建时间" + }, + "answer": "C", + "explanation": "value 是锁持有者的唯一标识。在解锁时,Lua 脚本会先比较 value 是否匹配,只有当前持有者才能成功释放锁。这防止了一个客户端释放另一个客户端持有的锁(例如锁已过期被新客户端获取后,旧客户端仍在尝试解锁的场景)。" + } + ], + "source": null, + "related": [] + }, + { + "id": "cr-002", + "type": "code_reading", + "difficulty": 4, + "tags": [ + "microservice", + "circuit-breaker" + ], + "question": "阅读以下熔断器状态机实现代码,回答子问题。", + "explanation": "本题考查熔断器的三态转换机制(Closed / Open / Half-Open),以及 failureThreshold、successThreshold、timeout 等参数的作用。", + "code": "package circuit\nimport (\n \"sync\"\n \"time\"\n)\ntype CircuitBreaker struct {\n mu sync.Mutex; state string; failureCount int\n failureThreshold, successCount, successThreshold int\n lastFailureTime time.Time; timeout time.Duration\n}\nfunc (cb *CircuitBreaker) Execute(fn func() error) error {\n cb.mu.Lock()\n if cb.state == \"open\" {\n if time.Since(cb.lastFailureTime) > cb.timeout {\n cb.state = \"half-open\"; cb.successCount = 0\n } else {\n cb.mu.Unlock()\n return ErrCircuitOpen\n }\n }\n cb.mu.Unlock()\n err := fn()\n cb.mu.Lock()\n defer cb.mu.Unlock()\n if err != nil {\n cb.failureCount++\n cb.lastFailureTime = time.Now()\n if cb.failureCount >= cb.failureThreshold {\n cb.state = \"open\"\n }\n } else if cb.state == \"half-open\" {\n cb.successCount++\n if cb.successCount >= cb.successThreshold {\n cb.state = \"closed\"; cb.failureCount = 0\n }\n } else {\n cb.failureCount = 0\n }\n return err\n}", + "language": "go", + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "当熔断器处于 open 状态且 timeout 时间已过,下次请求进入 Execute 时会发生什么?", + "options": { + "A": "直接返回错误,不做任何请求", + "B": "状态转为 half-open,执行实际请求进行探测", + "C": "状态直接转为 closed,恢复正常请求", + "D": "重置 failureCount 并重新开始计数" + }, + "answer": "B", + "explanation": "代码中明确处理了该场景:当 state == \"open\" 且 time.Since(lastFailureTime) > timeout 时,将状态设为 half-open 并重置 successCount,然后放行实际请求。这是熔断器的半开探测机制,用于测试下游服务是否已恢复。" + }, + { + "index": 2, + "type": "single_choice", + "question": "在 half-open 状态下,连续收到 successThreshold 次成功调用后熔断器如何转换?", + "options": { + "A": "保持 half-open 状态继续探测", + "B": "转为 open 状态", + "C": "转为 closed 状态,同时重置 failureCount", + "D": "转为 closed 状态,但保留 failureCount" + }, + "answer": "C", + "explanation": "代码中当 state == \"half-open\" 且 successCount >= successThreshold 时,将 state 设为 \"closed\" 并将 failureCount 重置为 0。这表示下游服务已恢复正常,熔断器关闭,恢复正常流量。" + }, + { + "index": 3, + "type": "short_answer", + "question": "代码中 open 状态下判断 timeout 后才放行探测请求,而不是直接拒绝所有请求。请说明这种设计的两个好处。", + "answer": "1) 为下游服务提供恢复窗口,避免永久阻断;2) 通过 half-open 状态渐进式恢复流量,避免雪崩", + "keywords": [ + "恢复窗口", + "half-open", + "渐进", + "雪崩", + "探测" + ], + "explanation": "第一,设置 timeout 后在 open 状态等待一段时间再探测,为下游服务提供了自我恢复的时间窗口,而不是永远拒绝所有请求。第二,通过 half-open 状态只放行部分请求进行探测,验证下游是否恢复,成功足够多次才切回 closed,这样可以渐进式恢复流量,避免突然大量请求冲击尚未完全恢复的服务造成雪崩。" + } + ], + "source": null, + "related": [] + }, + { + "id": "cr-003", + "type": "code_reading", + "difficulty": 5, + "tags": [ + "microservice", + "distributed-transaction", + "idempotent" + ], + "question": "阅读以下基于 Saga 模式的分布式事务实现代码,回答子问题。", + "explanation": "本题考查 Saga 模式的核心原理,包括正向执行链、补偿回滚、幂等设计等分布式事务关键概念。", + "code": "package saga\nimport (\n \"context\"\n \"fmt\"\n)\ntype Step struct {\n Name string\n Execute func(ctx context.Context, data *TxData) error\n Compensate func(ctx context.Context, data *TxData) error\n}\ntype TxData struct {\n OrderID string; Amount float64\n Results map[string]interface{}\n}\ntype Saga struct { steps []Step }\nfunc (s *Saga) Run(ctx context.Context, data *TxData) error {\n executed := make([]int, 0, len(s.steps))\n for i, step := range s.steps {\n if err := step.Execute(ctx, data); err != nil {\n fmt.Printf(\"Step [%s] failed: %v\\n\", step.Name, err)\n s.compensate(ctx, data, executed)\n return fmt.Errorf(\"saga aborted at %s: %w\", step.Name, err)\n }\n executed = append(executed, i)\n }\n return nil\n}\nfunc (s *Saga) compensate(ctx context.Context, data *TxData, executed []int) {\n for i := len(executed) - 1; i >= 0; i-- {\n step := s.steps[executed[i]]\n if err := step.Compensate(ctx, data); err != nil {\n fmt.Printf(\"Compensation for [%s] failed: %v\\n\", step.Name, err)\n }\n }\n}", + "language": "go", + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "compensate 方法使用倒序遍历 executed 切片来执行补偿,这样设计的核心原因是?", + "options": { + "A": "倒序遍历的性能比正序遍历更好", + "B": "保证补偿操作按照与正向执行相反的顺序执行,维持数据一致性", + "C": "只有倒序才能访问切片的最后一个元素", + "D": "Go 语言的限制,不支持正序遍历 map" + }, + "answer": "B", + "explanation": "Saga 模式的核心原则是补偿操作必须按正向操作的反序执行。例如:先扣库存再创建订单,补偿时应先取消订单再恢复库存。倒序遍历保证了这个语义,确保数据状态能够正确回滚。这类似于数据库事务日志的 undo 操作。" + }, + { + "index": 2, + "type": "short_answer", + "question": "代码中补偿失败时仅打印日志而不抛出异常。请分析这样设计的原因,以及生产环境中应如何处理补偿失败。", + "answer": "补偿失败属于不可自动恢复的异常,需要人工介入;生产中应记录失败详情到持久化存储并触发告警通知", + "keywords": [ + "人工介入", + "持久化", + "告警", + "重试", + "补偿日志" + ], + "explanation": "补偿操作可能因网络抖动、下游服务不可用等原因失败,此时 Saga 已无法自动回滚,继续重试可能陷入死循环。代码选择记录日志而不是中断,是因为补偿失败需要人工介入处理。生产环境中应:1) 将补偿失败的详细信息(步骤名、参数、错误信息)写入持久化存储(如数据库或消息队列);2) 触发告警通知运维人员;3) 提供手动重试或回滚的运维工具。" + }, + { + "index": 3, + "type": "single_choice", + "question": "TxData 中的 Results map 在 Saga 执行过程中起什么作用?", + "options": { + "A": "存储所有步骤的配置参数", + "B": "在步骤之间传递上下文数据,使后续步骤可以使用前序步骤的执行结果", + "C": "记录每个步骤的执行耗时", + "D": "缓存 RPC 调用的返回值以实现幂等" + }, + "answer": "B", + "explanation": "TxData.Results 是一个共享的上下文数据结构。在 Saga 的正向执行链中,每个步骤可以将执行结果写入 Results,后续步骤通过读取 Results 获取前序步骤的产出。例如创建订单步骤将 orderID 写入 Results,扣款步骤就可以使用该 orderID 进行关联。这是 Saga 实现步骤间数据传递的标准方式。" + } + ], + "source": null, + "related": [] + }, + { + "id": "cr-004", + "type": "code_reading", + "difficulty": 3, + "tags": [ + "microservice", + "rate-limiting" + ], + "question": "阅读以下基于令牌桶算法的限流器实现代码,回答子问题。", + "explanation": "本题考查令牌桶限流算法的核心机制,包括令牌生成速率、桶容量、以及突发流量处理。", + "code": "package ratelimit\nimport (\n \"sync\"\n \"time\"\n)\ntype TokenBucket struct {\n mu sync.Mutex; tokens float64; maxTokens float64\n refillRate float64; lastRefill time.Time\n}\nfunc NewTokenBucket(maxTokens, refillRate float64) *TokenBucket {\n return &TokenBucket{tokens: maxTokens, maxTokens: maxTokens,\n refillRate: refillRate, lastRefill: time.Now()}\n}\nfunc (tb *TokenBucket) Allow() bool {\n tb.mu.Lock()\n defer tb.mu.Unlock()\n tb.refill()\n if tb.tokens >= 1 { tb.tokens--; return true }\n return false\n}\nfunc (tb *TokenBucket) refill() {\n now := time.Now()\n tb.tokens += now.Sub(tb.lastRefill).Seconds() * tb.refillRate\n if tb.tokens > tb.maxTokens { tb.tokens = tb.maxTokens }\n tb.lastRefill = now\n}\nfunc (tb *TokenBucket) WaitN(n int) bool {\n tb.mu.Lock()\n defer tb.mu.Unlock()\n tb.refill()\n if tb.tokens >= float64(n) {\n tb.tokens -= float64(n)\n return true\n }\n return false\n}", + "language": "go", + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "refill 方法中将 tokens 限制在 maxTokens 以下,这个上限的作用是什么?", + "options": { + "A": "防止浮点数溢出导致程序崩溃", + "B": "防止空闲期间累积过多令牌,限制突发流量上限", + "C": "确保每次 refill 后至少有一个令牌", + "D": "限制令牌桶的并发访问数量" + }, + "answer": "B", + "explanation": "maxTokens 限制了令牌桶的最大容量,即允许累积的令牌上限。如果没有这个限制,空闲时间越长累积的令牌越多,恢复流量时会出现远超正常速率的突发请求,对下游服务造成冲击。maxTokens 限制了突发流量的上限,是令牌桶算法的流量整形特性。" + }, + { + "index": 2, + "type": "single_choice", + "question": "该限流器采用惰性 refill 策略(在 Allow/WaitN 时才计算新增令牌),而非后台定时器持续补充。这种设计的优势是?", + "options": { + "A": "计算精度更高,不会丢失任何令牌", + "B": "无需后台 goroutine,减少资源开销,无请求时不消耗 CPU", + "C": "支持更细粒度的限流配置", + "D": "允许在多线程环境下避免使用互斥锁" + }, + "answer": "B", + "explanation": "惰性 refill 在每次请求到来时才根据时间差计算应该补充的令牌数,不需要启动后台 goroutine 定时补充。这样在没有请求时完全不消耗 CPU 和内存资源,实现简单高效。虽然理论上可能有极微小的时间偏差,但在实际限流场景中完全可以接受。" + }, + { + "index": 3, + "type": "short_answer", + "question": "WaitN 方法允许一次性消耗 n 个令牌。请说明这种设计在什么业务场景下有实际意义,并举一个具体例子。", + "answer": "适用于批量处理或权重不同的请求场景,例如:上传大文件消耗3个令牌、普通API消耗1个令牌", + "keywords": [ + "批量", + "权重", + "大文件上传", + "不同成本", + "请求分级" + ], + "explanation": "不同的 API 接口消耗的系统资源不同,权重限流可以让高成本操作消耗更多令牌。例如一个文件上传接口可能需要占用更多带宽和存储资源,消耗 3 个令牌;而一个简单的查询接口只消耗 1 个令牌。这比简单的 QPS 限流更精细,能更好地保护后端服务资源。" + } + ], + "source": null, + "related": [] + }, + { + "id": "cr-005", + "type": "code_reading", + "difficulty": 4, + "tags": [ + "microservice", + "service-discovery", + "config-center" + ], + "question": "阅读以下服务注册与健康检查的实现代码,回答子问题。", + "explanation": "本题考查微服务架构中服务注册与发现的核心机制,包括服务注册、健康检查、实例剔除等。", + "code": "package discovery\nimport (\n \"sync\"\n \"time\"\n)\ntype ServiceInstance struct {\n ID string; Name string; Address string\n Port int; Status string\n}\ntype Registry struct {\n mu sync.RWMutex\n services map[string][]*ServiceInstance\n heartbeat map[string]time.Time; ttl time.Duration\n}\nfunc (r *Registry) Register(inst *ServiceInstance) {\n r.mu.Lock()\n defer r.mu.Unlock()\n r.services[inst.Name] = append(r.services[inst.Name], inst)\n r.heartbeat[inst.ID] = time.Now()\n}\nfunc (r *Registry) Heartbeat(id string) {\n r.mu.Lock()\n defer r.mu.Unlock()\n r.heartbeat[id] = time.Now()\n}\nfunc (r *Registry) evictLoop() {\n ticker := time.NewTicker(r.ttl / 2)\n defer ticker.Stop()\n for range ticker.C {\n r.mu.Lock()\n for _, instances := range r.services {\n for _, inst := range instances {\n if time.Now().Sub(r.heartbeat[inst.ID]) > r.ttl {\n inst.Status = \"DOWN\"\n }\n }\n }\n r.mu.Unlock()\n }\n}", + "language": "go", + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "evictLoop 的巡检间隔设置为 ttl/2 而非 ttl,这样设计的目的是?", + "options": { + "A": "提高代码的可读性", + "B": "确保在实例心跳过期后最多经过 ttl/2 时间就能被发现并标记为 DOWN", + "C": "减少内存占用", + "D": "避免与 Heartbeat 方法产生死锁" + }, + "answer": "B", + "explanation": "如果巡检间隔等于 ttl,则一个实例心跳过期后可能要等接近一个 ttl 的时间才会被发现和标记 DOWN,这意味着服务发现最长可能有 2×ttl 的延迟。使用 ttl/2 作为巡检间隔,可以在实例过期后最多 ttl/2 时间内就将其标记为不可用,将最长发现延迟控制在 1.5×ttl 以内。" + }, + { + "index": 2, + "type": "single_choice", + "question": "代码中 heartbeat map 使用实例 ID 作为 key,而非 ServiceInstance 指针。这种设计在什么场景下会产生问题?", + "options": { + "A": "当两个不同服务的实例使用相同的 ID 时会导致数据覆盖", + "B": "当实例被 Deregister 后,heartbeat 中的记录不会自动清理,造成内存泄漏", + "C": "无法支持实例的端口动态变更", + "D": "会导致 RWMutex 的读写竞争加剧" + }, + "answer": "B", + "explanation": "代码中 Register 方法同时写入 services 和 heartbeat,但缺少对应的 Deregister 方法来删除 heartbeat 中的记录。当实例下线后,services 列表可能通过某种方式清理,但 heartbeat map 中对应的 ID 和时间戳会一直保留,造成内存泄漏。生产环境中需要实现完整的注销逻辑,在实例下线时同步清理 heartbeat 记录。" + }, + { + "index": 3, + "type": "short_answer", + "question": "代码中 evictLoop 只是将实例标记为 DOWN 而不直接删除,请分析这种设计的两个原因。", + "answer": "1) 保留实例信息便于监控和排查问题;2) 防止短暂网络抖动导致的误删,实例恢复心跳后可重新UP", + "keywords": [ + "监控", + "排查", + "网络抖动", + "误删", + "恢复", + "最终一致" + ], + "explanation": "第一,保留 DOWN 状态的实例信息便于运维监控——可以统计服务可用率、发现不健康实例并排查根因,如果直接删除就丢失了这些信息。第二,短暂的网络抖动可能导致心跳暂时丢失,但实例本身是健康的。标记 DOWN 而不删除,实例下次心跳到来时可以恢复 UP 状态,避免因瞬时抖动导致实例被永久剔除和重新注册的开销。" + } + ], + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/interview-prep/distributed-microservice/fill_blank.json b/topics/interview-prep/distributed-microservice/fill_blank.json new file mode 100644 index 0000000..01f4812 --- /dev/null +++ b/topics/interview-prep/distributed-microservice/fill_blank.json @@ -0,0 +1,220 @@ +{ + "topic": "distributed-microservice", + "type": "fill_blank", + "schema_version": "1.0.0", + "generated": "2026-09-09T00:00:00+08:00", + "questions": [ + { + "id": "fb-001", + "type": "fill_blank", + "difficulty": 2, + "tags": [ + "microservice", + "service-discovery" + ], + "question": "在 CAP 定理中,Consul 和 ZooKeeper 都属于 ______ 型系统,即在分区容错性的前提下优先保证一致性和可用性中的______。", + "answer": [ + "CP", + "一致", + "一致性", + "C", + "consistency" + ], + "answer_rule": "any", + "explanation": "Consul 和 ZooKeeper 都遵循 CP 模型:在网络分区发生时优先保证一致性(Consistency),牺牲部分可用性。Consul 使用 Raft 协议实现一致性,ZooKeeper 使用 ZAB 协议。与之相对,Nacos 同时支持 AP 和 CP 模式(默认 AP),而 etcd 也是 CP 型系统。CP 系统在 Leader 选举期间会短暂不可用,但能保证所有节点读到的数据是一致的。", + "source": null, + "related": [] + }, + { + "id": "fb-002", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "microservice", + "load-balancing" + ], + "question": "在一致性哈希算法中,为了解决节点较少时数据倾斜的问题,通常引入______节点,即为每个真实节点创建多个虚拟节点映射到哈希环上。", + "answer": [ + "虚拟", + "Virtual", + "vnode", + "虚拟节点" + ], + "answer_rule": "any", + "explanation": "一致性哈希(Consistent Hashing)将节点和数据都映射到一个哈希环上,数据存储在顺时针方向遇到的第一个节点。当节点数量较少时,数据在环上的分布可能非常不均匀。引入虚拟节点(Virtual Node)后,每个物理节点对应环上的多个虚拟点,大幅改善了数据分布的均衡性。典型的实现如 Nginx 的一致性哈希模块、Redis Cluster 的槽分配都使用了这一技术。", + "source": null, + "related": [] + }, + { + "id": "fb-003", + "type": "fill_blank", + "difficulty": 2, + "tags": [ + "microservice", + "rate-limiting" + ], + "question": "令牌桶算法的工作原理是:系统以固定速率向桶中添加令牌,桶有最大容量限制,每次请求需要从桶中获取一个______才能通过,桶空时请求被拒绝。", + "answer": [ + "令牌", + "token", + "Token" + ], + "answer_rule": "any", + "explanation": "令牌桶(Token Bucket)算法的核心思想是:令牌以恒定速率(如每秒100个)持续注入桶中,桶容量(Burst Size)限制了突发流量的上限。每个请求到达时需取走一个令牌,桶中有令牌则放行,桶空则拒绝或排队。与漏桶(Leaky Bucket)算法的区别在于:令牌桶允许一定程度的突发流量(桶内积攒的令牌),而漏桶则强制以固定速率处理请求。Google Guava 的 RateLimiter 就是经典的令牌桶实现。", + "source": null, + "related": [] + }, + { + "id": "fb-004", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "microservice", + "circuit-breaker" + ], + "question": "熔断器(Circuit Breaker)有三种状态:正常放行请求的______状态、完全拒绝请求的______状态、以及尝试恢复的______状态。", + "answer": [ + "Closed", + "Open", + "Half-Open", + "关闭", + "打开", + "半开" + ], + "answer_rule": "any", + "explanation": "Circuit Breaker 模式包含三种状态:①Closed(关闭):正常状态,请求正常通过,同时统计失败率;②Open(打开):当失败率超过阈值,熔断器打开,所有请求直接被拒绝而不调用下游服务;③Half-Open(半开):经过一段超时时间后,允许少量探测请求通过,如果探测成功则回到 Closed 状态,失败则回到 Open 状态。Hystrix、Resilience4j 和 Sentinel 都实现了这一状态机,是微服务容错的核心机制。", + "source": null, + "related": [] + }, + { + "id": "fb-005", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "microservice", + "config-center" + ], + "question": "Apollo 配置中心通过______机制实现配置的实时推送,客户端监听长轮询(Long Polling)端点,配置变更后服务端主动通知客户端拉取最新配置,从而实现配置热更新。", + "answer": [ + "长轮询", + "Long Polling", + "long polling", + "long-polling" + ], + "answer_rule": "any", + "explanation": "Apollo 配置中心的配置热更新流程为:①客户端启动时拉取配置并缓存到本地;②客户端对 Config Service 发起长轮询(Long Polling)请求,该请求不会立即返回;③当配置发生变更时,Config Service 立即结束长轮询并返回变更通知;④客户端收到通知后重新拉取最新配置并更新本地缓存和内存。这种方式避免了客户端频繁轮询带来的性能损耗,同时保证了配置变更的实时性。与之对比,Nacos 使用 UDP Push + 长轮询的混合推送模式,etcd 则基于 Watch 机制实现。", + "source": null, + "related": [] + }, + { + "id": "fb-006", + "type": "fill_blank", + "difficulty": 2, + "tags": [ + "microservice", + "idempotent" + ], + "question": "为了保证接口的幂等性,常用的一种方案是:客户端在请求前先从服务端获取一个唯一的______,随请求一起提交,服务端在处理请求前检查该令牌是否已被使用。", + "answer": [ + "Token", + "token", + "令牌" + ], + "answer_rule": "any", + "explanation": "Token 机制是实现接口幂等的常用方案:①客户端在发起操作前,先调用获取 Token 的接口,服务端生成唯一 Token 存入 Redis/数据库并返回;②客户端将该 Token 放在请求参数或 Header 中提交;③服务端收到请求后检查 Token 是否存在,存在则删除 Token 并执行业务逻辑,不存在则说明是重复请求,拒绝处理。这种方案适用于创建类操作。其他幂等方案还包括:幂等键(基于业务唯一 ID 去重)、去重表(数据库唯一索引约束)等。", + "source": null, + "related": [] + }, + { + "id": "fb-007", + "type": "fill_blank", + "difficulty": 4, + "tags": [ + "microservice", + "distributed-lock" + ], + "question": "Redis 的分布式锁使用 SET key value NX PX timeout 命令实现,其中 NX 表示仅当 key______时才设置,PX 表示以______为单位设置过期时间。为了解决锁过期但业务未完成的问题,需要引入______机制(如 Redisson 的 WatchDog)来自动续期。", + "answer": [ + "不存在", + "not exists", + "毫秒", + "ms", + "millisecond", + "锁续期", + "看门狗", + "WatchDog", + "watchdog" + ], + "answer_rule": "any", + "explanation": "Redis 分布式锁的核心实现要点:①SET NX 保证互斥性——只有 key 不存在时才能设置成功;②PX(毫秒)/ EX(秒)设置过期时间,防止持有锁的进程崩溃导致死锁;③锁续期(WatchDog)机制:Redisson 的 WatchDog 在锁的过期时间到达前 1/3 时自动续期,默认续期到 30 秒。如果业务执行时间超过最大锁持有时间(默认 30 秒),WatchDog 停止续期,锁自动释放。这解决了「锁提前过期」的经典问题。注意:使用 value(如 UUID+线程ID)来标识锁持有者,释放锁时需验证身份,避免误删他人的锁。", + "source": null, + "related": [] + }, + { + "id": "fb-008", + "type": "fill_blank", + "difficulty": 4, + "tags": [ + "microservice", + "distributed-transaction" + ], + "question": "TCC(Try-Confirm-Cancel)分布式事务模式中,分为三个阶段:Try 阶段进行资源的______和校验;Confirm 阶段执行真正的业务操作;Cancel 阶段______预留的资源。", + "answer": [ + "预留", + "冻结", + "检查", + "reservation", + "预留资源" + ], + "answer_rule": "any", + "explanation": "TCC 模式将每个服务的业务逻辑拆分为三个步骤:①Try:对业务资源进行检查和预留(如冻结库存、冻结账户余额),但不执行真正的业务逻辑;②Confirm:如果所有服务的 Try 都成功,执行确认操作,真正扣减库存和金额;③Cancel:如果任何一个 Try 失败,执行补偿操作,释放所有已预留的资源。TCC 是强一致性方案,性能优于 2PC,但对业务侵入性较高(需要实现三个接口)。典型的框架包括 Seata 的 TCC 模式、Hmily 等。", + "source": null, + "related": [] + }, + { + "id": "fb-009", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "microservice", + "service-discovery" + ], + "question": "服务注册与发现的健康检查中,______模式是指服务主动向注册中心发送心跳报文以证明自己存活;______模式是指注册中心主动向服务实例发起探测(如 HTTP GET /health)来判断服务状态。", + "answer": [ + "客户端心跳", + "Client Heartbeat", + "心跳", + "Heartbeat", + "服务端主动探测", + "Server-side Probe", + "主动探测", + "Push" + ], + "answer_rule": "any", + "explanation": "健康检查的两种主要模式:①Push 模式(客户端心跳):服务实例定期向注册中心发送心跳包,注册中心在一定时间内未收到心跳则将实例标记为不健康。Consul 支持 TTL 心跳模式,Eureka 默认使用心跳续约;②Pull 模式(服务端主动探测):注册中心按配置间隔主动探测服务实例的健康端点。Consul 支持 HTTP/TCP/gRPC/Script 等多种探测方式,Kubernetes 使用 liveness/readiness Probe。实际生产中,很多系统同时使用两种模式以提高可靠性。", + "source": null, + "related": [] + }, + { + "id": "fb-010", + "type": "fill_blank", + "difficulty": 4, + "tags": [ + "microservice", + "distributed-transaction" + ], + "question": "在基于消息队列的分布式事务方案中,______模式是指在本地事务中同时执行业务操作和写入消息表,然后由后台任务扫描消息表将消息发送到消息队列;而 RocketMQ 的______事务消息方案则利用 Half Message(预消息)实现,先发送半消息,在本地事务执行成功后再提交或回滚消息。", + "answer": [ + "本地消息表", + "Outbox", + "outbox", + "Transactional Message", + "事务消息" + ], + "answer_rule": "any", + "explanation": "两种基于消息的最终一致性方案:①本地消息表(Transactional Outbox):业务操作和消息写入在同一个本地事务中,保证原子性;后台轮询线程扫描未发送的消息并投递到 MQ,消费端消费后回调确认。实现简单但依赖轮询频率,有一定延迟。②RocketMQ 事务消息:①生产者发送 Half Message(预消息)到 Broker,消费者此时不可见;②生产者执行本地事务;③根据本地事务结果向 Broker 发送 Commit(消费者可见)或 Rollback(删除消息);④如果 Broker 长时间未收到确认,会主动回查生产者的本地事务状态。事务消息方案更优雅,延迟更低,但依赖 MQ 的事务支持能力。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/interview-prep/distributed-microservice/meta.json b/topics/interview-prep/distributed-microservice/meta.json new file mode 100644 index 0000000..c830416 --- /dev/null +++ b/topics/interview-prep/distributed-microservice/meta.json @@ -0,0 +1,25 @@ +{ + "slug": "distributed-microservice", + "name": "分布式微服务架构", + "description": "服务注册与发现、负载均衡、限流熔断、配置中心、幂等控制、分布式锁、分布式事务", + "tags": [], + "question_files": { + "single_choice": "single_choice.json", + "true_false": "true_false.json", + "fill_blank": "fill_blank.json", + "short_answer": "short_answer.json", + "code_reading": "code_reading.json" + }, + "stats": { + "total": 45, + "by_type": { + "single_choice": 15, + "true_false": 10, + "fill_blank": 10, + "short_answer": 5, + "code_reading": 5 + } + }, + "created": "2026-09-09", + "updated": "2026-09-09" +} \ No newline at end of file diff --git a/topics/interview-prep/distributed-microservice/short_answer.json b/topics/interview-prep/distributed-microservice/short_answer.json new file mode 100644 index 0000000..70477d7 --- /dev/null +++ b/topics/interview-prep/distributed-microservice/short_answer.json @@ -0,0 +1,156 @@ +{ + "topic": "distributed-microservice", + "type": "short_answer", + "schema_version": "1.0.0", + "generated": "2026-09-09T00:00:00+08:00", + "questions": [ + { + "id": "sa-001", + "type": "short_answer", + "difficulty": 3, + "tags": [ + "microservice", + "service-discovery" + ], + "question": "请对比 Consul、Nacos、Etcd 和 ZooKeeper 四种服务注册与发现工具,从 CAP 定理的视角分析它们的一致性模型差异,并说明各自的健康检查机制有何不同。", + "answer": "从 CAP 定理角度:\n1. ZooKeeper 是 CP 系统,基于 ZAB 协议保证强一致性,任何写操作需要过半节点确认(Leader 选举期间不可用),在网络分区时牺牲可用性。\n2. Consul 是 CP 系统,基于 Raft 协议,强一致性写入需 Leader 确认,但支持多数据中心。\n3. Etcd 同样是 CP 系统,基于 Raft 协议,提供线性一致性读写。\n4. Nacos 支持 AP 和 CP 两种模式切换:临时实例(ephemeral)采用 AP 模式(Distro 协议),持久实例采用 CP 模式(Raft),可根据场景灵活选择。\n\n健康检查差异:\n- Consul:支持 HTTP、TCP、gRPC、Script、TTL 多种方式,支持主动和被动健康检查。\n- Nacos:临时实例通过客户端心跳(默认5秒)维持,持久实例通过服务端主动探测(TCP/HTTP/MySQL)。\n- Etcd:本身不提供内建健康检查,需借助外部组件(如 Kubernetes 的 Readiness Probe)。\n- ZooKeeper:基于 Session 和临时节点(Ephemeral Node)机制,客户端断连后临时节点自动删除,类似心跳检测。", + "keywords": [ + "CP", + "AP", + "Raft", + "ZAB", + "Distro", + "心跳", + "临时节点", + "健康检查", + "Raft协议", + "Nacos双模式" + ], + "scoring_rubric": "满分10分。CP/AP定理分类正确(3分):ZK/Consul/Etcd为CP、Nacos支持AP+CP各得满分;一致性协议准确(3分):提及Raft/ZAB/Distro;健康检查机制描述(4分):至少描述3种工具的健康检查方式且准确。", + "explanation": "这道题考察对分布式系统基础理论(CAP)与主流注册中心实现细节的理解。核心区别在于:CP 系统在网络分区时牺牲可用性保证一致性,而 Nacos 的双模式设计是其差异化优势。健康检查机制直接影响服务的上下线时效和故障检测灵敏度,是生产环境中的关键配置项。", + "source": null, + "related": [] + }, + { + "id": "sa-002", + "type": "short_answer", + "difficulty": 3, + "tags": [ + "microservice", + "load-balancing", + "rate-limiting" + ], + "question": "在一个高并发的微服务场景中,某核心接口 QPS 达到 5000,后端部署了 8 个实例。请分别设计负载均衡策略和限流方案,并说明为什么选择这些策略,以及如何处理实例故障时的流量转移。", + "answer": "负载均衡策略设计:\n1. 采用一致性哈希作为基础策略,以请求参数(如用户ID)作为哈希键,确保同一用户的请求路由到同一实例,提升本地缓存命中率。\n2. 引入虚拟节点解决节点数少时的数据倾斜问题。\n3. 结合加权轮询:根据各实例的实时负载(CPU、内存、RT)动态调整权重,实现柔性负载均衡。\n4. 客户端负载均衡(如 Spring Cloud LoadBalancer/Ribbon)减少网络跳数,服务端负载均衡(如 Nginx/Envoy)便于统一管控。\n\n限流方案设计:\n1. 总入口层:采用滑动窗口计数器(Sentinel),对整个接口设置 6000 QPS 的总限流阈值(留 20% 余量)。\n2. 单实例:每个实例分配 750 QPS 的令牌桶限流(0.2秒预热),应对瞬时突发流量。\n3. 分布式限流:使用 Redis + Lua 脚本实现全局限流,原子操作保证并发安全;或使用 Sentinel 集群限流模式。\n4. 多级限流:网关层(Sentinel/Gateway)粗粒度 + 服务层(RateLimiter)细粒度。\n\n实例故障处理:\n1. 一致性哈希环上故障节点的流量自动顺时针转移到下一个健康节点。\n2. 配合健康检查(3秒超时)快速摘除故障实例。\n3. 限流阈值动态调整:实例数减少时,按比例缩减各实例限流配额,防止过载。", + "keywords": [ + "一致性哈希", + "虚拟节点", + "加权轮询", + "令牌桶", + "滑动窗口", + "Sentinel", + "Redis+Lua", + "客户端负载均衡", + "服务端负载均衡", + "健康检查" + ], + "scoring_rubric": "满分12分。负载均衡策略设计合理(4分):一致性哈希+虚拟节点+动态权重至少提及2项;限流方案完整(4分):至少包含全局+单实例两层限流,令牌桶/滑动窗口/Sentinel选其二;故障转移处理(2分):哈希环流量漂移+健康检查摘除;方案合理性说明(2分):有清晰的选型理由。", + "explanation": "此题综合考察负载均衡与限流的实际工程设计能力。一致性哈希解决会话粘性问题,滑动窗口解决流量精确统计,令牌桶解决突发流量平滑,Sentinel 解决分布式统一管控。故障时的流量自动转移是高可用的关键保障。", + "source": null, + "related": [] + }, + { + "id": "sa-003", + "type": "short_answer", + "difficulty": 4, + "tags": [ + "microservice", + "circuit-breaker", + "config-center" + ], + "question": "请描述 Circuit Breaker(熔断器)模式的三态状态机(Closed、Open、Half-Open)的工作原理和状态流转条件,并对比 Hystrix、Sentinel 和 Resilience4j 三者的核心差异。如果将熔断器与配置中心(如 Nacos/Apollo)结合使用,如何实现熔断参数的动态下发和热更新?", + "answer": "一、熔断器三态状态机:\n1. Closed(关闭):正常状态,请求正常通过,熔断器持续统计失败率。当失败率超过阈值(如 50%)且在滑动窗口内的请求量达到最小请求数(如 20),状态切换为 Open。\n2. Open(打开):熔断状态,所有请求直接被拒绝(快速失败),返回降级响应(Fallback)。经过设定的超时时间(如 5 秒)后,自动进入 Half-Open。\n3. Half-Open(半开):试探状态,允许有限数量的请求通过。如果这些请求成功率恢复到阈值以上,回到 Closed;如果仍然失败,回到 Open。\n\n二、三者核心差异:\n- Hystrix(Netflix,已停更):基于 RxJava/线程池隔离,默认 10 秒滑动窗口,提供 Hystrix Dashboard 可视化。\n- Resilience4j(替代 Hystrix):基于装饰器模式,轻量级,支持 Circuit Breaker、RateLimiter、Retry、Bulkhead 等模块组合使用,函数式友好,线程开销低。\n- Sentinel(阿里):除了熔断外,还集成了流量控制、系统自适应保护、热点参数限流;支持实时监控 Dashboard 和动态规则推送,与 Spring Cloud / Dubbo 深度集成。\n\n三、与配置中心结合实现热更新:\n1. Nacos 方案:在 Nacos 中创建熔断规则配置(如 JSON 格式的 CircuitBreaker Rule),Sentinel 通过 Nacos DataSource 监听配置变更,实时拉取新规则并生效,无需重启。\n2. Apollo 方案:在 Apollo 配置中心维护熔断参数(失败率阈值、超时窗口等),Resilience4j 或 Sentinel 通过 @ApolloConfigListener 监听变更事件,动态重建 CircuitBreaker 实例。\n3. 核心原理:配置中心提供长轮询/WebSocket 通知机制,客户端监听器收到变更后原子地替换内存中的规则对象,实现秒级热更新。", + "keywords": [ + "Closed", + "Open", + "Half-Open", + "失败率阈值", + "滑动窗口", + "快速失败", + "Fallback", + "Hystrix", + "Sentinel", + "Resilience4j", + "Nacos DataSource", + "ApolloConfigListener", + "热更新", + "动态规则推送" + ], + "scoring_rubric": "满分15分。三态状态机描述完整准确(5分):三态定义各1分、流转条件各0.5分;三者对比准确(5分):各框架核心特征至少提到2点;配置中心热更新方案(5分):Nacos/Apollo至少描述一种完整链路,包含监听机制和生效原理。", + "explanation": "熔断器是微服务容错的核心机制,三态状态机保证了系统的优雅降级与自动恢复。Hystrix 已停更但概念经典,Sentinel 功能最全面,Resilience4j 最轻量。与配置中心结合实现参数热更新,是生产环境中快速调整容错策略、应对流量突变的关键能力。", + "source": null, + "related": [] + }, + { + "id": "sa-004", + "type": "short_answer", + "difficulty": 4, + "tags": [ + "microservice", + "idempotent", + "distributed-lock" + ], + "question": "在一个电商下单场景中,用户可能因网络重试导致重复提交订单。请设计一个完整的幂等控制方案,并说明如何结合分布式锁(Redis RedLock / ZooKeeper)保证并发下的幂等性。请比较 Redis RedLock 和 ZooKeeper 分布式锁的实现差异,包括锁续期和可重入性支持。", + "answer": "一、幂等控制方案设计:\n1. 幂等键(Idempotency Key):客户端在请求中携带唯一幂等键(如 UUID),服务端在处理前查询该键是否已存在。若存在则直接返回之前的结果;若不存在则执行业务逻辑并记录。\n2. Token 机制:调用方先调用获取 Token 接口获得一次性令牌,随请求一起提交。服务端在 Redis 中用 Lua 脚本原子性地检查并删除 Token,成功则处理业务,失败则拒绝重复请求。\n3. 去重表(数据库唯一索引):在订单表中设置 request_id UNIQUE 索引,重复插入时数据库抛出异常,应用层捕获后返回已有结果。\n4. 状态机幂等:通过订单状态流转(如 CREATED → PAID → SHIPPED),前置校验当前状态是否允许该操作,防止重复状态变更。\n\n二、与分布式锁结合:\n1. 加锁时机:在幂等键校验通过后、执行核心业务前,对业务主键(如商品ID+用户ID)加分布式锁。\n2. 处理流程:获取锁 → 二次检查幂等标记(Double Check)→ 执行业务 → 写入幂等标记 → 释放锁。\n\n三、Redis RedLock vs ZooKeeper 分布式锁:\n1. Redis RedLock:\n - 实现:基于 Redisson,向 N 个独立 Redis 节点(通常 5 个)依次请求加锁,超过半数(N/2+1)成功且总耗时小于锁有效期则获锁成功。\n - 锁续期:通过 WatchDog 机制(默认30秒),后台线程每10秒自动续期,业务完成后主动释放。\n - 可重入性:支持,基于 Hash 结构记录重入次数,同一线程可多次加锁。\n - 争议:Martin Kleppmann 指出 RedLock 在时钟跳跃等场景下的安全性问题,但实际生产中 Redisson 的 WatchDog 机制已大幅缓解。\n\n2. ZooKeeper 分布式锁:\n - 实现:基于临时顺序节点(Ephemeral Sequential),每个客户端创建 /lock/seq-xxx 节点,只锁定序号最小的节点;后续节点监听前一个节点的删除事件(避免惊群效应)。\n - 锁续期:基于 Session 心跳,客户端定期发送心跳维持 Session,Session 过期则临时节点自动删除,锁自动释放。\n - 可重入性:通过在节点中记录持有者标识(IP+线程ID+重入次数)实现可重入。\n - 优势:CP 保证,数据一致性更强;Session 机制天然支持锁自动释放,安全性高于 Redis。", + "keywords": [ + "幂等键", + "Token机制", + "去重表", + "唯一索引", + "状态机", + "RedLock", + "WatchDog", + "可重入", + "临时顺序节点", + "Session心跳", + "Double Check", + "Lua脚本" + ], + "scoring_rubric": "满分15分。幂等方案完整(5分):至少描述3种幂等手段且正确;分布式锁结合(3分):包含加锁+Double Check流程;RedLock描述(4分):半数节点+WatchDog续期+可重入;ZooKeeper描述(3分):临时顺序节点+Session续期+可重入。", + "explanation": "幂等控制是分布式系统的核心设计要求,尤其在支付、下单等涉及资金操作的场景。分布式锁是实现幂等的手段之一,但需配合幂等键/去重表双重保障。RedLock 追求性能但有一致性争议,ZooKeeper 追求一致性但性能较低,选型需根据业务场景权衡。", + "source": null, + "related": [] + }, + { + "id": "sa-005", + "type": "short_answer", + "difficulty": 5, + "tags": [ + "microservice", + "distributed-transaction" + ], + "question": "在一个涉及订单服务、库存服务、支付服务的电商场景中,下单操作需要同时完成:创建订单、扣减库存、发起支付。请对比 2PC、TCC、Saga 和本地消息表四种分布式事务方案的原理、优缺点,并说明你会选择哪种方案以及理由。", + "answer": "一、四种方案对比:\n\n1. 2PC(两阶段提交):\n - 原理:协调者(TM)向所有参与者(RM)发送 Prepare 请求,参与者执行本地事务但不提交并回复 Ready/Abort;若全部 Ready,则发送 Commit,否则发送 Rollback。\n - 优点:强一致性,数据可靠。\n - 缺点:同步阻塞(Prepare 后资源锁定)、协调者单点故障、数据不一致风险(Commit 阶段部分参与者提交失败)、性能差(不适合高并发)。\n\n2. TCC(Try-Confirm-Cancel):\n - 原理:Try 阶段预留资源(如冻结库存、冻结金额);Confirm 阶段确认提交(扣减冻结资源);Cancel 阶段回滚释放冻结资源。需要为每个操作实现三个接口。\n - 优点:不依赖数据库锁,性能优于 2PC;业务层控制,灵活度高。\n - 缺点:开发成本高(每个服务三个接口);Try 失败时需处理悬挂(空回滚)和幂等问题;资源锁定时间长。\n\n3. Saga:\n - 原理:将长事务拆分为一系列本地事务 T1→T2→T3...,每个 Ti 有对应的补偿事务 Ci。正向执行:T1→T2→T3;若 T3 失败,则逆向补偿:C2→C1。可通过编排(Orchestration)或协同(Choreography)模式实现。\n - 优点:无资源锁定,性能好;适合长流程业务。\n - 缺点:最终一致性,中间状态可能可见;补偿逻辑复杂(如补偿操作本身失败);缺乏隔离性(需要通过语义锁等方式补偿)。\n\n4. 本地消息表:\n - 原理:在订单服务本地事务中,同时写入业务数据和消息表(同一数据库事务保证原子性)。后台定时任务扫描消息表,将消息发送到 MQ,下游消费者处理后回调确认。消息表可定期清理。\n - 优点:实现简单,不依赖特殊中间件;利用数据库事务保证一致性;可靠投递。\n - 缺点:引入消息轮询的性能开销;消息表数据量大时需要分库分表;与业务耦合(消息表结构随业务变化)。\n\n二、方案选择:\n选择 Saga + 本地消息表的组合方案。理由:\n1. 电商下单是典型的长流程业务,涉及多个微服务调用,不适合 2PC 的同步阻塞模型。\n2. Saga 编排模式(如使用 Seata 的 Saga 模式或 Temporal)可以清晰定义正向流程和补偿流程,流程可视化,便于排查问题。\n3. 订单创建和库存扣减使用本地消息表保证最终一致性,支付结果通过回调确认。\n4. 对于库存扣减的中间状态可见问题,可在查询时通过订单状态过滤未完成的事务。\n5. TCC 开发成本过高,电商场景更看重交付速度和性能,不值得投入每个服务三个接口的开发量。", + "keywords": [ + "2PC", + "TCC", + "Saga", + "本地消息表", + "最终一致性", + "补偿事务", + "Try-Confirm-Cancel", + "编排模式", + "协同模式", + "悬挂", + "空回滚", + "语义锁", + "Seata", + "中间状态" + ], + "scoring_rubric": "满分18分。四种方案原理描述各2分(8分):每种方案至少正确描述核心流程;优缺点对比各1分(4分):每种至少提到一个优点和一个缺点;方案选择有理有据(4分):选择合理且理由充分,至少从性能/一致性/开发成本三个维度论证;补充中间状态处理方案(2分):提到语义锁或状态过滤。", + "explanation": "分布式事务是微服务架构中最复杂的难题之一,核心是在一致性、可用性、性能之间做权衡。2PC 保证强一致但性能差,TCC 灵活但开发成本高,Saga 适合长流程但只有最终一致性,本地消息表简单可靠但引入额外复杂度。实际选型需结合业务场景(实时性要求、并发量、团队能力)综合判断,不存在银弹方案。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/interview-prep/distributed-microservice/single_choice.json b/topics/interview-prep/distributed-microservice/single_choice.json new file mode 100644 index 0000000..908b91f --- /dev/null +++ b/topics/interview-prep/distributed-microservice/single_choice.json @@ -0,0 +1,308 @@ +{ + "topic": "distributed-microservice", + "type": "single_choice", + "schema_version": "1.0.0", + "generated": "2026-09-09T00:00:00+08:00", + "questions": [ + { + "id": "sc-001", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "microservice", + "service-discovery" + ], + "question": "在微服务架构中,服务注册与发现的核心作用是什么?", + "options": { + "A": "加密服务间的通信数据", + "B": "让服务消费者能够动态地找到服务提供者的网络地址", + "C": "限制单个服务的并发访问量", + "D": "自动为服务分配数据库连接" + }, + "answer": "B", + "explanation": "服务注册与发现的核心作用是维护一个服务实例的注册表,使服务消费者无需硬编码地址即可动态找到可用的服务提供者实例,实现服务间的解耦和弹性伸缩。", + "source": null, + "related": [] + }, + { + "id": "sc-002", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "microservice", + "service-discovery" + ], + "question": "以下关于服务注册中心的说法,哪一项是错误的?", + "options": { + "A": "Eureka 采用 AP 模型,支持自我保护机制,在网络分区时优先保证可用性", + "B": "Consul 支持健康检查,同时提供键值存储能力", + "C": "ZooKeeper 采用 CP 模型,在 leader 选举期间服务注册可能短暂不可用", + "D": "Nacos 默认采用 AP 模型,不支持配置中心功能" + }, + "answer": "D", + "explanation": "Nacos 默认采用 AP 模式(Distro 协议),但同时支持 CP 模式(Raft 协议)的切换,且 Nacos 本身就内置了配置中心功能,因此「Nacos 不支持配置中心功能」这一说法是错误的。", + "source": null, + "related": [] + }, + { + "id": "sc-003", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "microservice", + "load-balancing" + ], + "question": "负载均衡策略中,「加权轮询」与「简单轮询」的主要区别是什么?", + "options": { + "A": "加权轮询会优先将请求发送到响应时间最短的节点", + "B": "加权轮询根据各节点的权重按比例分配请求,权重高的节点获得更多流量", + "C": "简单轮询不记录请求分配的历史", + "D": "加权轮询不适用于异构服务器集群" + }, + "answer": "B", + "explanation": "加权轮询在简单轮询的基础上引入了权重的概念,根据每台服务器的处理能力(如 CPU、内存)分配不同的权重值,权重越高的节点按比例承担更多请求。而简单轮询是均等分配,不考虑节点差异。", + "source": null, + "related": [] + }, + { + "id": "sc-004", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "microservice", + "load-balancing" + ], + "question": "在 Ribbon/LoadBalancer 的负载均衡策略中,「最少连接数」策略的适用场景是?", + "options": { + "A": "请求处理时间均匀且服务节点性能一致的场景", + "B": "请求处理时间差异大、后端节点性能不一致的场景", + "C": "所有请求必须严格按顺序处理的场景", + "D": "服务节点数量固定且网络延迟恒定的场景" + }, + "answer": "B", + "explanation": "最少连接数策略会将新请求分配给当前活跃连接数最少的节点,这在请求处理时间差异大、节点性能不一致时特别有效,因为连接数少并不代表负载低(可能刚分配了一个耗时长的请求),但总体上能更好地平衡实际负载。", + "source": null, + "related": [] + }, + { + "id": "sc-005", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "microservice", + "rate-limiting" + ], + "question": "令牌桶算法与漏桶算法的核心区别是什么?", + "options": { + "A": "令牌桶只能限制总请求数,漏桶可以控制请求速率", + "B": "令牌桶允许一定程度的突发流量,漏桶以固定速率匀速处理请求", + "C": "漏桶算法比令牌桶更节省内存", + "D": "令牌桶适用于出站流量控制,漏桶适用于入站流量控制" + }, + "answer": "B", + "explanation": "漏桶算法以固定的速率匀速处理请求,任何超出桶容量的请求都会被丢弃或排队;令牌桶算法以固定速率向桶中放入令牌,桶满时丢弃令牌,但允许积攒令牌以应对突发流量,因此能更好地容忍短时间的流量突增。", + "source": null, + "related": [] + }, + { + "id": "sc-006", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "microservice", + "rate-limiting" + ], + "question": "在分布式限流方案中,Redis + Lua 脚本实现滑动窗口限流的主要优势是?", + "options": { + "A": "避免了每次请求都需要多次 Redis 通信的网络开销,通过 Lua 脚本保证操作的原子性", + "B": "完全不需要 Redis,纯本地内存即可实现", + "C": "只能实现单机限流,不支持分布式场景", + "D": "滑动窗口不需要占用额外存储空间" + }, + "answer": "A", + "explanation": "Redis 执行 Lua 脚本时是原子性的(单线程执行),可以将滑动窗口的「查询-判断-更新」多个步骤合并为一次网络往返,既保证了原子性避免竞态条件,又减少了 Redis 的网络通信次数,提高了性能。", + "source": null, + "related": [] + }, + { + "id": "sc-007", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "microservice", + "circuit-breaker" + ], + "question": "熔断器模式中,从「关闭状态」切换到「打开状态」的触发条件通常是?", + "options": { + "A": "服务响应时间超过 1 秒", + "B": "在设定的时间窗口内,失败率或慢调用比例超过阈值", + "C": "服务实例数量少于 3 个", + "D": "服务的 CPU 使用率超过 80%" + }, + "answer": "B", + "explanation": "熔断器从关闭状态切换到打开状态的典型条件是在一个统计时间窗口内,错误率(如 5xx 比例)或慢调用比例超过了预设阈值(如 Hystrix 默认 50% 错误率)。这保护了调用方不被持续的失败拖垮。", + "source": null, + "related": [] + }, + { + "id": "sc-008", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "microservice", + "circuit-breaker" + ], + "question": "Resilience4j 与 Hystrix 相比,以下哪项描述是正确的?", + "options": { + "A": "Resilience4j 依赖 Netflix 的 Archaius 配置库进行运行时动态配置", + "B": "Resilience4j 采用函数式编程风格,基于 Vavr 库,不依赖线程池隔离", + "C": "Hystrix 仍处于活跃维护状态,推荐新项目使用", + "D": "Resilience4j 不支持 RateLimiter 功能,只能做熔断" + }, + "answer": "B", + "explanation": "Resilience4j 采用轻量级的函数式设计,依赖 Vavr 函数式库,不强制使用线程池隔离(可选 Semaphore 或 ThreadPool 两种隔离策略),体积更小。而 Hystrix 已停止维护,Resilience4j 支持熔断、限流、重试、隔离等多种功能。", + "source": null, + "related": [] + }, + { + "id": "sc-009", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "microservice", + "config-center" + ], + "question": "配置中心实现「配置热更新」的核心机制是什么?", + "options": { + "A": "每次请求配置时重新启动服务实例", + "B": "通过长轮询或发布订阅机制,在配置变更时主动推送通知给客户端", + "C": "将配置写入数据库,客户端每次查询后缓存", + "D": "通过定时任务每 5 分钟从远程拉取一次配置" + }, + "answer": "B", + "explanation": "配置热更新的核心是「变更推送」机制:客户端建立长轮询(如 Nacos 的 Long Polling)或订阅通道(如 Spring Cloud Config + Bus),当配置变更时由配置中心主动通知客户端刷新本地配置,无需重启服务即可生效。", + "source": null, + "related": [] + }, + { + "id": "sc-010", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "microservice", + "config-center" + ], + "question": "在 Spring Cloud 中,当配置中心不可用时,服务启动会使用本地缓存的配置文件。这种设计体现了什么原则?", + "options": { + "A": "单一职责原则", + "B": "服务降级与容错设计——在外部依赖不可用时仍能保证服务基本可用", + "C": "开闭原则", + "D": "依赖倒置原则" + }, + "answer": "B", + "explanation": "这体现了服务降级与容错的设计思想:配置中心作为外部依赖,一旦不可用不应阻塞服务的启动和运行。通过本地配置文件作为 fallback(降级方案),保证服务仍能以最后已知的有效配置正常工作,提高系统的可用性和韧性。", + "source": null, + "related": [] + }, + { + "id": "sc-011", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "microservice", + "idempotent" + ], + "question": "在分布式系统中,以下哪种请求天然具有幂等性?", + "options": { + "A": "POST 创建订单", + "B": "PUT 更新用户资料为指定值", + "C": "POST 支付扣款(累加金额)", + "D": "POST 点赞(累加计数)" + }, + "answer": "B", + "explanation": "幂等性是指同一个请求执行多次与执行一次的效果相同。PUT 操作是「设置为指定值」,无论执行多少次结果都是相同的。而 POST 创建订单会产生多个订单,累加扣款和累加计数多次执行会导致金额或次数不正确。", + "source": null, + "related": [] + }, + { + "id": "sc-012", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "microservice", + "idempotent" + ], + "question": "使用「幂等键 + Redis SETNX」实现接口幂等控制时,如何处理以下场景:请求 A 获取锁后处理失败(未释放锁),此时请求 B 携带相同幂等键到达,该如何处理?", + "options": { + "A": "直接拒绝请求 B,返回处理中", + "B": "立即删除请求 A 的幂等键,让请求 B 重新执行", + "C": "等待请求 A 完成后自动释放锁", + "D": "使用无锁的数据库唯一索引方案替代 Redis SETNX" + }, + "answer": "A", + "explanation": "在幂等控制中,如果请求 A 已获取幂等键但处理失败(未释放),请求 B 携带相同幂等键到达时,应当返回「处理中」或拒绝重复提交。正确的做法是给幂等键设置合理的过期时间(TTL),超时后自动释放,同时配合人工重试机制。直接删除其他请求的幂等键会导致并发安全问题。", + "source": null, + "related": [] + }, + { + "id": "sc-013", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "microservice", + "distributed-lock" + ], + "question": "Redis 分布式锁中,设置锁的过期时间(TTL)的主要目的是什么?", + "options": { + "A": "提高锁的获取性能", + "B": "防止持锁进程崩溃后无法释放锁,导致其他进程永远无法获取锁", + "C": "减少 Redis 的内存占用", + "D": "保证锁的互斥性" + }, + "answer": "B", + "explanation": "设置过期时间是防止「死锁」的关键措施:如果持有锁的进程在执行过程中崩溃或宕机,无法主动释放锁,其他进程将永远等待。TTL 作为安全兜底,确保锁最终会被释放。这也是 Redlock 和 Redisson 分布式锁的重要组成部分。", + "source": null, + "related": [] + }, + { + "id": "sc-014", + "type": "single_choice", + "difficulty": 5, + "tags": [ + "microservice", + "distributed-lock" + ], + "question": "Redlock 算法要求在 N 个独立的 Redis 主节点上获取锁,成功条件是什么?", + "options": { + "A": "在任意 1 个节点上获取锁即可", + "B": "在超过 N/2 个节点上获取锁,且总耗时小于锁的 TTL", + "C": "在所有 N 个节点上都获取锁", + "D": "在主节点和所有从节点上都获取锁" + }, + "answer": "B", + "explanation": "Redlock 算法要求在 N 个(通常为 5 个)独立 Redis 主节点上尝试获取锁,当在超过半数(>N/2)节点上成功获取锁,且获取锁的总耗时小于锁的 TTL 时,才算成功获取分布式锁。这样即使部分节点故障,锁的安全性仍有保障。", + "source": null, + "related": [] + }, + { + "id": "sc-015", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "microservice", + "distributed-transaction" + ], + "question": "在 Saga 模式中,当某一步操作失败时,需要执行「补偿事务」来回滚。以下关于 Saga 的说法,哪一项是正确的?", + "options": { + "A": "补偿事务必须使用与正向操作完全相同的逻辑,只是反向执行", + "B": "Saga 的补偿事务是最终一致性方案,每个补偿操作本身也应该是幂等的", + "C": "Saga 模式能保证像数据库事务那样的强一致性", + "D": "Saga 中的补偿事务只在网络故障时才需要执行" + }, + "answer": "B", + "explanation": "Saga 是一种最终一致性方案,每个正向操作对应一个补偿操作。由于补偿操作可能因网络等原因被重复执行,因此每个补偿操作必须设计为幂等的。补偿事务的逻辑不一定是正向操作的简单反转,而是根据业务语义设计的(如库存不足时的补偿可能是取消预留而非简单加回)。Saga 不提供强一致性保证。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/interview-prep/distributed-microservice/true_false.json b/topics/interview-prep/distributed-microservice/true_false.json new file mode 100644 index 0000000..a9725b9 --- /dev/null +++ b/topics/interview-prep/distributed-microservice/true_false.json @@ -0,0 +1,148 @@ +{ + "topic": "distributed-microservice", + "type": "true_false", + "schema_version": "1.0.0", + "generated": "2026-09-09T00:00:00+08:00", + "questions": [ + { + "id": "tf-001", + "type": "true_false", + "difficulty": 2, + "tags": [ + "microservice", + "service-discovery" + ], + "question": "在 CAP 定理中,ZooKeeper 属于 CP 系统,在网络分区时会牺牲可用性以保证一致性。", + "answer": true, + "explanation": "ZooKeeper 采用 ZAB(ZooKeeper Atomic Broadcast)协议,保证强一致性(CP)。当发生网络分区时,少数派分区的节点无法选举出 Leader,会拒绝写入请求,从而牺牲了可用性(A)。相比之下,Nacos 支持 AP/CP 模式切换,Consul 默认也是 CP 模式(支持最终一致性的 Consistency Mode)。这也是为什么在服务发现场景中,部分团队倾向于使用支持 AP 模式的注册中心。", + "source": null, + "related": [] + }, + { + "id": "tf-002", + "type": "true_false", + "difficulty": 3, + "tags": [ + "microservice", + "service-discovery" + ], + "question": "Nacos 同时支持 CP 和 AP 两种模式,默认使用 AP 模式来保证服务注册的高可用性。", + "answer": false, + "explanation": "Nacos 确实支持 CP 和 AP 两种模式的切换(通过 Distro 协议实现 AP,Raft 协议实现 CP),但其默认模式并非 AP。在 Nacos 1.x 中,临时实例(ephemeral=true,默认)使用 AP 模式的 Distro 协议,而永久实例使用 CP 模式的 Raft 协议。在 Nacos 2.x 中,服务注册默认走 gRPC 长连接的临时实例模式,本质上是 AP 行为。所以说「默认 AP」需要区分版本和实例类型,但更准确的说法是 Nacos 的临时实例默认使用 AP 模式,永久实例使用 CP 模式。严格来说,该陈述过于简化,判定为错误。", + "source": null, + "related": [] + }, + { + "id": "tf-003", + "type": "true_false", + "difficulty": 2, + "tags": [ + "microservice", + "load-balancing" + ], + "question": "一致性哈希算法在节点增减时,只需要重新映射约 1/n 的数据(n 为节点数),非常适合有状态服务的负载均衡。", + "answer": true, + "explanation": "一致性哈希(Consistent Hashing)将节点和数据映射到同一个哈希环上。当新增或移除一个节点时,只有该节点与逆时针方向相邻节点之间的数据需要重新分配,平均影响约 1/n 的数据量。这大大减少了传统哈希取模在节点变化时导致的大规模数据迁移。在微服务架构中,一致性哈希常用于有状态服务(如分布式缓存、会话管理)的负载均衡。实践中通常引入虚拟节点(Virtual Nodes)来解决数据倾斜问题,使负载更均匀。", + "source": null, + "related": [] + }, + { + "id": "tf-004", + "type": "true_false", + "difficulty": 3, + "tags": [ + "microservice", + "rate-limiting" + ], + "question": "漏桶算法和令牌桶算法的主要区别在于:漏桶算法允许突发流量以固定速率处理,而令牌桶算法则以恒定速率匀速处理请求。", + "answer": false, + "explanation": "该陈述将两种算法的特性说反了。令牌桶(Token Bucket)以恒定速率向桶中添加令牌,请求需要消耗令牌才能通过,桶中可以积累令牌,因此允许一定程度的突发流量(突发时消耗积累的令牌)。漏桶(Leaky Bucket)以固定速率处理(漏出)请求,无论输入速率多高,输出速率始终恒定,起到「削峰填谷」的作用。简言之:令牌桶允许突发,漏桶强制平滑。Sentinel 等框架中常用的滑动窗口计数器则结合了时间窗口统计的优势,更适合精确限流场景。", + "source": null, + "related": [] + }, + { + "id": "tf-005", + "type": "true_false", + "difficulty": 3, + "tags": [ + "microservice", + "circuit-breaker" + ], + "question": "Circuit Breaker(熔断器)的三种状态中,Half-Open 状态允许少量请求通过以探测下游服务是否恢复,这是熔断器从 Open 转为 Closed 的必要中间状态。", + "answer": true, + "explanation": "Circuit Breaker 的标准状态机为:Closed(正常)→ Open(熔断,拒绝所有请求)→ Half-Open(半开,允许少量探测请求通过)→ Closed(恢复)或 Open(继续熔断)。当熔断发生后,经过一段超时时间,熔断器进入 Half-Open 状态,放行有限数量的试探请求。如果试探请求成功率恢复到阈值以上,则回到 Closed 状态;否则重新回到 Open 状态。这个机制避免了服务刚恢复时因大量请求涌入而再次崩溃。Hystrix、Resilience4j、Sentinel 均实现了此模式。", + "source": null, + "related": [] + }, + { + "id": "tf-006", + "type": "true_false", + "difficulty": 4, + "tags": [ + "microservice", + "config-center" + ], + "question": "Apollo 配置中心通过长轮询(Long Polling)机制实现配置的实时推送,而 Nacos 则采用 WebSocket 推送配置变更通知。", + "answer": true, + "explanation": "Apollo(携程开源)采用 HTTP 长轮询机制:客户端发起一个 HTTP 请求后,服务端会 hold 住该连接一段时间(如 60 秒),期间若有配置变更则立即返回,超时后客户端重新发起轮询。这种方式兼容性好,不依赖特定协议。Nacos 1.x 配置推送基于长轮询,但在 Nacos 2.x 中,配置变更通知已结合 gRPC 长连接实现更实时的推送。两者都支持配置热更新——客户端监听配置变更后可以不重启应用就生效新配置。Apollo 还提供了命名空间(Namespace)隔离和灰度发布能力。", + "source": null, + "related": [] + }, + { + "id": "tf-007", + "type": "true_false", + "difficulty": 2, + "tags": [ + "microservice", + "idempotent" + ], + "question": "实现接口幂等性的 Token 机制要求客户端在调用业务接口前先获取一个全局唯一的 Token,业务接口在执行成功后必须删除该 Token,以防止重复提交。", + "answer": true, + "explanation": "Token 机制是实现幂等控制的经典方案:1)客户端先向服务端请求一个全局唯一 Token(如 UUID);2)将 Token 随业务请求一起发送;3)服务端验证 Token 是否存在,存在则执行业务逻辑并删除 Token,不存在则拒绝请求(说明是重复提交)。该机制的关键在于 Token 的原子性校验和删除(通常使用 Redis 的 SET NX + DEL 或 Lua 脚本保证原子性)。幂等键(如业务唯一 ID + 去重表)是另一种常用方案,适用于无法预先获取 Token 的场景。", + "source": null, + "related": [] + }, + { + "id": "tf-008", + "type": "true_false", + "difficulty": 4, + "tags": [ + "microservice", + "distributed-lock" + ], + "question": "Redis 的 RedLock 算法通过向多个独立的 Redis 实例加锁,只要多数(N/2+1)节点加锁成功即视为获取锁成功,因此可以完全替代 ZooKeeper 在分布式锁场景中的使用。", + "answer": false, + "explanation": "RedLock 算法的核心思路是正确的:向 N 个独立 Redis 实例请求加锁,超过半数成功且总耗时未超过锁的有效期则获取成功。但 RedLock 在学术界和工程界存在广泛争议(Martin Kleppmann 的著名批评文章《How to do distributed locking》)。主要问题包括:1)时钟漂移可能导致锁安全性失效;2)GC 暂停或网络延迟可能导致客户端认为自己持有锁但实际已过期;3)RedLock 不具备 ZooKeeper 那样的强一致性和顺序保证。在对一致性要求极高的场景(如金融交易),ZooKeeper 或 etcd 的分布式锁仍是更可靠的选择。RedLock 适合对性能要求高、可容忍极小概率不一致的场景。", + "source": null, + "related": [] + }, + { + "id": "tf-009", + "type": "true_false", + "difficulty": 3, + "tags": [ + "microservice", + "distributed-lock" + ], + "question": "基于 ZooKeeper 的分布式锁天然支持锁续期机制,客户端无需额外处理即可防止锁在业务未完成时过期。", + "answer": false, + "explanation": "基于 ZooKeeper 的分布式锁使用临时顺序节点(Ephemeral Sequential Node)实现:客户端创建临时节点并监听前一个节点,当前序节点删除时获取锁。临时节点的生命周期与客户端会话(Session)绑定——如果客户端与 ZooKeeper 断开连接,Session 超时后临时节点会自动删除,从而释放锁。但这并不等同于「锁续期」:1)如果客户端发生长时间 GC 或网络分区但未断开 Session,锁不会自动释放(这可能造成死锁);2)ZooKeeper 本身没有提供类似 Redis 的 TTL 续期 API。锁续期(Watchdog 机制)是 Redisson 等 Redis 分布式锁客户端提供的能力,通过后台线程定期延长锁的过期时间。在 ZooKeeper 场景中,需要应用层自行实现类似的续约逻辑来应对长时间任务。", + "source": null, + "related": [] + }, + { + "id": "tf-010", + "type": "true_false", + "difficulty": 4, + "tags": [ + "microservice", + "distributed-transaction" + ], + "question": "TCC(Try-Confirm-Cancel)分布式事务方案中,Try 阶段需要预留业务资源,如果 Try 阶段部分参与者失败,则需要对所有已成功执行 Try 的参与者执行 Cancel 操作进行回滚。", + "answer": true, + "explanation": "TCC 是一种业务层面的分布式事务解决方案,将每个参与者拆分为三个阶段:Try(资源检查与预留)、Confirm(确认执行,使用预留资源完成业务)、Cancel(取消,释放预留资源)。其核心要求是 Confirm 和 Cancel 操作必须是幂等的。当 Try 阶段有参与者失败时,事务协调器(或框架如 Seata 的 TCC 模式)会对所有已完成 Try 的参与者发起 Cancel 调用,释放冻结的资源。这与 2PC 的回滚机制类似,但 TCC 的优势在于将锁粒度从数据库层面提升到业务层面(如冻结金额而非锁定行),提高了并发性能。代价是业务侵入性强,每个操作需要实现三个接口。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/interview-prep/go-java-concurrency/code_reading.json b/topics/interview-prep/go-java-concurrency/code_reading.json new file mode 100644 index 0000000..5683f53 --- /dev/null +++ b/topics/interview-prep/go-java-concurrency/code_reading.json @@ -0,0 +1,263 @@ +{ + "topic": "go-java-concurrency", + "type": "code_reading", + "schema_version": "1.0.0", + "generated": "2026-09-09T16:21:56+08:00", + "questions": [ + { + "id": "cr-001", + "type": "code_reading", + "difficulty": 3, + "tags": [ + "go", + "channel", + "select" + ], + "question": "分析以下Go代码片段,理解select多路复用与channel交互行为:", + "code": "func main() {\n ch := make(chan int, 1)\n quit := make(chan struct{})\n go func() {\n time.Sleep(100 * time.Millisecond)\n ch <- 42\n close(quit)\n }()\n select {\n case v := <-ch:\n fmt.Println(\"received:\", v)\n case <-quit:\n fmt.Println(\"quit\")\n }\n // 第二个select\n select {\n case v := <-ch:\n fmt.Println(\"second:\", v)\n case <-quit:\n fmt.Println(\"quit again\")\n }\n}", + "language": "go", + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "这段程序的输出最可能是什么?", + "options": { + "A": "received: 42,然后 quit again", + "B": "quit,然后 quit again", + "C": "received: 42,然后 second: 42", + "D": "编译错误" + }, + "answer": "A", + "explanation": "第一个select等待100ms后goroutine向ch发送42并close(quit)。由于ch有缓冲1,send先于close执行,第一个select匹配case v:=<-ch,输出'received: 42'。之后ch已被消费为空,quit已关闭。第二个select时ch为空无法接收,quit已关闭可立即接收,输出'quit again'。" + }, + { + "index": 2, + "type": "short_answer", + "question": "如果将ch的缓冲大小从1改为0(无缓冲channel),程序的行为会有什么变化?", + "answer": "第一个select可能匹配quit分支而非ch分支", + "keywords": [ + "无缓冲", + "同步阻塞", + "send和receive必须同时就绪", + "竞争" + ], + "scoring_rubric": "正确指出无缓冲channel需要发送方和接收方同时就绪(2分),说明goroutine中send在close之前但第一个select可能先匹配quit(1分),说明行为不确定/依赖调度(1分)", + "explanation": "无缓冲channel的send操作必须有对应的receive就绪才能完成。goroutine执行ch <- 42时会阻塞,直到主goroutine的select准备接收。但select也会检查quit channel(此时quit未close),所以send和select存在竞争。如果select先选中quit分支则退出,否则选中ch分支完成send。行为不确定。" + } + ], + "explanation": "本题考查Go channel的select多路复用机制。select会同时监听所有case,当多个case就绪时随机选择一个。有缓冲channel的send不阻塞(缓冲区未满),而无缓冲channel的send必须有对应的receive。close已关闭的channel会立即返回零值。", + "source": null, + "related": [] + }, + { + "id": "cr-002", + "type": "code_reading", + "difficulty": 4, + "tags": [ + "go", + "sync", + "rwmutex" + ], + "question": "分析以下Go代码片段,找出sync.RWMutex使用中的潜在问题:", + "code": "type Cache struct {\n mu sync.RWMutex\n data map[string]string\n}\n\nfunc (c *Cache) Get(key string) string {\n c.mu.RLock()\n defer c.mu.RUnlock()\n if v, ok := c.data[key]; ok {\n return v\n }\n c.mu.RUnlock()\n c.mu.Lock()\n defer c.mu.Unlock()\n // double check\n if v, ok := c.data[key]; ok {\n return v\n }\n c.data[key] = \"default\"\n return \"default\"\n}", + "language": "go", + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "这段代码在key不存在时会出现什么问题?", + "options": { + "A": "死锁,因为重复调用了RUnlock", + "B": "panic: sync: unlock of unlocked RWMutex", + "C": "正常运行,无任何问题", + "D": "数据竞争,但不会崩溃" + }, + "answer": "B", + "explanation": "当key不存在时,第一个RLock/RUnlock的defer已经unlock了读锁,接着又显式调用c.mu.RUnlock()试图再次释放读锁,导致panic: sync: unlock of unlocked RWMutex。即使没有defer,RUnlock后再次RUnlock也会panic。" + }, + { + "index": 2, + "type": "short_answer", + "question": "请写出修正后的Get方法,正确实现读锁升级为写锁的逻辑。", + "answer": "func (c *Cache) Get(key string) string {\n c.mu.RLock()\n if v, ok := c.data[key]; ok {\n c.mu.RUnlock()\n return v\n }\n c.mu.RUnlock()\n c.mu.Lock()\n defer c.mu.Unlock()\n if v, ok := c.data[key]; ok {\n return v\n }\n c.data[key] = \"default\"\n return \"default\"\n}", + "keywords": [ + "先释放读锁", + "再获取写锁", + "double check", + "defer释放" + ], + "scoring_rubric": "正确释放读锁后再获取写锁(2分),保留double check避免重复写(1分),写操作在写锁保护下(1分),使用defer释放锁(1分)", + "explanation": "Go的RWMutex不支持锁升级(直接从读锁升级为写锁)。正确做法是先释放读锁,再获取写锁,然后进行double check确认数据是否已被其他goroutine写入。" + } + ], + "explanation": "本题考查RWMutex的正确使用模式。Go的sync.RWMutex不支持锁升级,读锁和写锁互斥,必须先释放读锁再获取写锁。常见的错误是重复释放锁导致panic。", + "source": null, + "related": [] + }, + { + "id": "cr-003", + "type": "code_reading", + "difficulty": 4, + "tags": [ + "go", + "goroutine", + "pprof" + ], + "question": "分析以下Go代码片段,识别goroutine泄漏的场景:", + "code": "func queryAll(urls []string) []Result {\n results := make(chan Result, len(urls))\n for _, url := range urls {\n go func(u string) {\n resp, err := http.Get(u)\n if err != nil {\n results <- Result{URL: u, Err: err}\n return\n }\n defer resp.Body.Close()\n body, _ := io.ReadAll(resp.Body)\n results <- Result{URL: u, Body: body}\n }(url)\n }\n var collected []Result\n for i := 0; i < len(urls); i++ {\n collected = append(collected, <-results)\n }\n return collected\n}", + "language": "go", + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "如果调用queryAll时传入的urls切片包含100个URL,但http.Get对某些URL长时间超时未响应,会发生什么?", + "options": { + "A": "queryAll会立即返回空结果", + "B": "queryAll会阻塞直到所有goroutine完成(包括超时的)", + "C": "超时的goroutine会被自动回收", + "D": "程序会panic" + }, + "answer": "B", + "explanation": "主goroutine的for循环会依次从results channel接收len(urls)个结果。如果某些goroutine因网络超时长时间阻塞在http.Get上,主goroutine会在对应的<-results处阻塞等待,直到所有goroutine都完成。没有设置context超时控制。" + }, + { + "index": 2, + "type": "short_answer", + "question": "如何改进这段代码以避免goroutine泄漏?至少给出两种方案。", + "answer": "方案一:使用context.WithTimeout控制整体超时。方案二:使用select+time.After或context控制单个请求超时。方案三:使用errgroup管理goroutine。", + "keywords": [ + "context.WithTimeout", + "http.NewRequestWithContext", + "errgroup", + "select", + "超时控制" + ], + "scoring_rubric": "提出context超时方案(2分),说明具体实现方式(2分),解释为什么能防止泄漏(1分)", + "explanation": "方案1:使用context.WithTimeout创建带超时的context传给http.NewRequestWithContext,超时后HTTP请求取消。方案2:使用errgroup.Group的WithContext方法自动管理goroutine生命周期。方案3:增加done channel或select+timer实现超时退出。" + }, + { + "index": 3, + "type": "single_choice", + "question": "使用pprof定位此类goroutine泄漏时,最应该查看哪个profile?", + "options": { + "A": "cpu profile", + "B": "heap profile", + "C": "goroutine profile", + "D": "block profile" + }, + "answer": "C", + "explanation": "goroutine profile可以显示当前所有goroutine的调用栈,能直接看到泄漏的goroutine数量和它们阻塞在哪个函数调用上,是排查goroutine泄漏的首选工具。block profile显示锁竞争和channel阻塞,但不如goroutine profile直观。" + } + ], + "explanation": "本题考查goroutine泄漏的典型场景——缺乏超时控制导致goroutine无法退出。正确做法是通过context控制请求超时,并使用pprof的goroutine profile定位泄漏。", + "source": null, + "related": [] + }, + { + "id": "cr-004", + "type": "code_reading", + "difficulty": 4, + "tags": [ + "java", + "volatile", + "jmm" + ], + "question": "分析以下Java代码片段,理解volatile语义与happens-before规则:", + "code": "public class VisibilityExample {\n private volatile boolean running = true;\n private int counter = 0;\n\n public void startLoop() {\n new Thread(() -> {\n while (running) {\n counter++;\n }\n System.out.println(\"Stopped. counter=\" + counter);\n }).start();\n }\n\n public void stop() {\n running = false;\n }\n}", + "language": "java", + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "关于这段代码,以下哪个说法是正确的?", + "options": { + "A": "running是volatile的,所以counter++也是线程安全的", + "B": "running的修改能被子线程看到,但counter可能存在可见性问题", + "C": "由于while循环的存在,子线程永远无法看到running=false", + "D": "这段代码完全没有并发问题" + }, + "answer": "B", + "explanation": "volatile保证running的修改对子线程可见(happens-before语义),所以子线程最终会退出循环。但counter不是volatile的,counter++(读-改-写)不是原子操作,子线程读取counter的值可能存在可见性问题——子线程可能一直看到旧值。不过由于只有一个线程修改counter,实际不会有数据竞争。选项B正确指出counter可能存在可见性问题(虽然单线程写不会出错)。" + }, + { + "index": 2, + "type": "short_answer", + "question": "如果在main线程中频繁调用stop()方法,而同时子线程在执行counter++,counter的最终值是否准确?请从JMM的角度解释原因。", + "answer": "counter的值不保证准确。counter++不是原子操作,包含read、increment、write三步。虽然本例中只有子线程一个线程修改counter,但如果counter被多个线程修改,就会出现数据竞争。从JMM角度看,非volatile变量的修改不保证对其他线程可见。本例中只有一个线程写counter,所以值是准确的,但如果设计意图是多个线程共享counter,则需要AtomicInteger或synchronized。", + "keywords": [ + "原子性", + "read-modify-write", + "volatile不保证原子性", + "AtomicInteger", + "数据竞争" + ], + "scoring_rubric": "指出counter++非原子操作(2分),说明volatile不保证原子性(1分),提出AtomicInteger/synchronized解决方案(1分),区分单线程写和多线程写场景(1分)", + "explanation": "volatile只保证可见性和有序性,不保证原子性。counter++是复合操作(读-改-写),在多线程环境下会出现竞态条件。应使用AtomicInteger的getAndIncrement()或synchronized保护counter。" + } + ], + "explanation": "本题考查Java内存模型中volatile的核心语义。volatile保证变量的可见性(一个线程的修改对其他线程可见)和有序性(禁止指令重排序),但不保证原子性。counter++这样的复合操作仍需要额外同步机制。", + "source": null, + "related": [] + }, + { + "id": "cr-005", + "type": "code_reading", + "difficulty": 5, + "tags": [ + "java", + "thread", + "forkjoinpool" + ], + "question": "分析以下Java代码片段,理解ThreadPoolExecutor与ForkJoinPool的区别:", + "code": "// 方案A: ThreadPoolExecutor\nExecutorService poolA = new ThreadPoolExecutor(\n 4, 8, 60L, TimeUnit.SECONDS,\n new LinkedBlockingQueue<>(1000)\n);\n\n// 方案B: ForkJoinPool\nForkJoinPool poolB = new ForkJoinPool(\n Runtime.getRuntime().availableProcessors()\n);\n\n// 提交任务\npoolA.submit(() -> {\n // CPU密集型计算\n return heavyComputation();\n});\n\npoolB.submit(() -> {\n // 可分治的CPU密集型计算\n return recursiveComputation();\n});", + "language": "java", + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "当poolA的线程都在执行任务且队列已满时,新提交的任务会怎样?", + "options": { + "A": "创建新的线程来执行", + "B": "任务在调用者的线程中执行(CallerRunsPolicy)", + "C": "抛出RejectedExecutionException(使用默认拒绝策略时)", + "D": "任务被丢弃但不抛异常" + }, + "answer": "C", + "explanation": "ThreadPoolExecutor的默认拒绝策略是AbortPolicy,当线程数达到maximumPoolSize且工作队列已满时,会抛出RejectedExecutionException。注意:本例中核心线程数4 < 最大线程数8,队列是无界的(capacity=1000),但实际上LinkedBlockingQueue(1000)有界。当8个线程都在忙且队列1000个位置都满了,才会触发拒绝策略。" + }, + { + "index": 2, + "type": "single_choice", + "question": "ForkJoinPool相比ThreadPoolExecutor的核心优势是什么?", + "options": { + "A": "支持更多的线程数", + "B": "工作窃取算法允许空闲线程从其他线程的队列中取任务,提高CPU利用率", + "C": "不需要传入Runnable或Callable任务", + "D": "自动管理线程的生命周期" + }, + "answer": "B", + "explanation": "ForkJoinPool的核心优势是工作窃取(work-stealing)算法。每个工作线程有自己的双端队列(Deque),当一个线程的队列为空时,它可以'窃取'其他线程队列尾部的任务。这比ThreadPoolExecutor的单一共享队列减少了锁竞争,特别适合递归分治任务(如ForkJoinTask的fork/join模式)。" + }, + { + "index": 3, + "type": "short_answer", + "question": "在什么场景下应该选择ForkJoinPool而不是ThreadPoolExecutor?请举例说明。", + "answer": "适合ForkJoinPool的场景:1.任务可以分解为子任务的分治问题(如排序、搜索);2.大量小任务且执行时间差异大(工作窃取能平衡负载);3.递归任务。适合ThreadPoolExecutor的场景:1.独立的异步任务(如HTTP请求);2.任务之间没有依赖关系;3.需要精确控制队列和拒绝策略。", + "keywords": [ + "分治", + "递归", + "工作窃取", + "负载均衡", + "独立任务", + "队列策略" + ], + "scoring_rubric": "正确描述ForkJoinPool适用场景(2分),正确描述ThreadPoolExecutor适用场景(1分),给出具体例子(1分),提到工作窃取的负载均衡优势(1分)", + "explanation": "ForkJoinPool设计用于可递归分解的任务,其工作窃取算法在任务执行时间不均匀时表现优异。ThreadPoolExecutor更通用,适合独立的异步任务。Java 8+的parallelStream底层就使用ForkJoinPool.commonPool()。" + } + ], + "explanation": "本题考查Java线程池的核心参数和ForkJoinPool的工作原理。ThreadPoolExecutor通过核心线程数、最大线程数、队列和拒绝策略来控制任务执行。ForkJoinPool通过工作窃取算法实现更高效的并行计算,特别适合分治递归任务。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/interview-prep/go-java-concurrency/fill_blank.json b/topics/interview-prep/go-java-concurrency/fill_blank.json new file mode 100644 index 0000000..380f2ee --- /dev/null +++ b/topics/interview-prep/go-java-concurrency/fill_blank.json @@ -0,0 +1,186 @@ +{ + "topic": "go-java-concurrency", + "type": "fill_blank", + "schema_version": "1.0.0", + "generated": "2026-09-09T16:21:56+08:00", + "questions": [ + { + "id": "fb-001", + "type": "fill_blank", + "difficulty": 2, + "tags": [ + "go", + "gmp" + ], + "question": "Go语言GMP调度模型中,P是G和M之间的调度上下文,当一个P的本地队列已满时,新创建的G会被放入______队列,由其他P通过work-stealing机制窃取执行。", + "answer": [ + "全局", + "全局队列", + "global queue", + "global" + ], + "explanation": "GMP模型中,每个P拥有一个本地可运行G队列(local run queue)。当本地队列容量(默认256)满载时,新创建的G会被放入全局队列(global queue)。调度器会在以下时机从全局队列取出G:1)本地队列为空时;2)每调度61次从全局队列取一个G,防止全局队列中的G被饿死。work-stealing则是当某个P的本地队列为空时,它会尝试从其他P的本地队列窃取一半的G来运行,从而实现负载均衡。", + "source": null, + "related": [] + }, + { + "id": "fb-002", + "type": "fill_blank", + "difficulty": 2, + "tags": [ + "java", + "thread" + ], + "question": "Java中创建线程的三种主要方式分别是继承______类、实现Runnable接口和实现Callable接口。", + "answer": [ + "Thread" + ], + "explanation": "Java提供三种创建线程的方式:1)继承Thread类,重写run()方法,直接调用start()启动;2)实现Runnable接口,将任务与线程解耦,传入Thread构造器;3)实现Callable接口并配合FutureTask,支持返回结果和抛出异常。其中Runnable的run()没有返回值且不能抛出受检异常,而Callable的call()可以返回泛型结果。实际开发中,推荐使用线程池(ExecutorService)而非直接创建线程,以实现资源复用和更好的并发控制。", + "source": null, + "related": [] + }, + { + "id": "fb-003", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "go", + "channel" + ], + "question": "Go的channel底层数据结构hchan中,环形缓冲区______用于存储有缓冲channel中待发送的数据,sendx和recvx分别指向其发送和接收位置。", + "answer": [ + "buf", + "ringbuf" + ], + "explanation": "hchan是Go channel的底层实现结构,核心字段包括:buf(环形数组缓冲区,仅用于有缓冲channel)、sendx和recvx(分别标记环形缓冲区的发送和接收位置,用于实现FIFO)、sendq和recvq(sudog链表,分别记录因发送/接收阻塞的goroutine)。当channel无缓冲时,buf为nil,发送和接收直接在两个goroutine之间传递数据。环形缓冲区的设计使得sendx到达末尾后可以回绕到数组起始位置,高效利用内存。", + "source": null, + "related": [] + }, + { + "id": "fb-004", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "java", + "volatile", + "jmm" + ], + "question": "Java内存模型中,volatile变量保证可见性的核心原理是:对volatile变量的写操作会立即刷新到______,读操作会从该处重新加载,从而禁止了线程工作内存对该变量的缓存。", + "answer": [ + "主内存", + "主存", + "main memory" + ], + "explanation": "Java内存模型(JMM)定义了主内存(main memory)和工作内存(working memory)的抽象概念。每个线程拥有自己的工作内存(对应CPU缓存或寄存器),对变量的读写首先在工作内存中进行,再同步到主内存。volatile关键字的关键语义包括:1)保证可见性——写volatile变量时立即刷新到主内存,读时从主内存重新加载;2)禁止指令重排序——通过内存屏障(Memory Barrier)确保volatile写之前的操作不会被重排到写之后,volatile读之后的操作不会被重排到读之前。但注意volatile不能保证原子性,例如i++对volatile变量的自增仍然不是线程安全的。", + "source": null, + "related": [] + }, + { + "id": "fb-005", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "go", + "sync" + ], + "question": "Go的sync.WaitGroup有三个核心方法:Add用于增加计数器,Done用于减少计数器(等同于Add(-1)),当计数器归零时,调用______方法阻塞等待的goroutine会被唤醒。", + "answer": [ + "Wait" + ], + "explanation": "WaitGroup是Go中用于等待一组goroutine完成的同步原语。典型用法:主goroutine在启动工作goroutine前调用Add(n)设置计数器,每个工作goroutine完成时调用Done()(内部执行Add(-1)),主goroutine调用Wait()阻塞直到计数器归零。底层实现中,WaitGroup使用原子操作维护state1和state2两个字段,分别存储信号量和计数器值。需要注意的是:Add的调用必须在Wait之前或在Wait的goroutine内部调用,不能在工作goroutine中调用Add,否则可能在Wait已经开始等待后才Add,导致竞态条件。", + "source": null, + "related": [] + }, + { + "id": "fb-006", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "java", + "synchronized", + "cas" + ], + "question": "Java synchronized关键字在JDK 6之后引入了锁升级机制,其升级路径为:无锁 → ______ → 轻量级锁(自旋) → 重量级锁,这一优化大幅提升了在不同竞争程度下的同步性能。", + "answer": [ + "偏向锁", + "biased locking", + "Biased Locking" + ], + "explanation": "JDK 6引入的锁升级(lock escalation)是synchronized性能优化的关键:1)偏向锁——当只有一个线程访问同步块时,在对象头的Mark Word中记录线程ID,后续该线程进入时只需CAS检查ID,无需任何同步操作;2)轻量级锁——当第二个线程尝试获取锁时,偏向锁撤销,升级为轻量级锁,通过CAS将Mark Word复制到栈帧的Lock Record中,适合短时间、低竞争的场景,未获取到锁的线程会通过自旋(adaptive spinning)等待;3)重量级锁——当自旋超过一定次数或竞争激烈时,升级为重量级锁,依赖操作系统的Mutex Lock实现,未获取到锁的线程会被阻塞挂起。锁只能升级不能降级(JDK 15默认关闭了偏向锁)。", + "source": null, + "related": [] + }, + { + "id": "fb-007", + "type": "fill_blank", + "difficulty": 4, + "tags": [ + "go", + "gmp" + ], + "question": "GMP调度模型中,当goroutine执行系统调用(如文件I/O)被阻塞时,M会与P解绑,P会被交给其他空闲的M或创建新的M来继续执行本地队列中的G,这一机制被称为______。", + "answer": [ + "hand off", + "handoff", + "hand-off" + ], + "explanation": "Hand off(移交)是GMP调度模型中处理M阻塞的核心机制。当某个M上的G执行阻塞性系统调用时,M会进入阻塞状态,此时P(及其本地队列中的G)不能跟着一起等待。调度器会将P从这个阻塞的M上解绑(hand off),交给另一个空闲的M来继续执行P本地队列中的G。如果此时没有空闲的M,则会创建一个新M。当系统调用返回后,原M会尝试获取一个空闲的P来继续运行,如果没有空闲P,则该G会被放入全局队列。这一机制确保了一个阻塞的系统调用不会导致整个P上的所有goroutine都被阻塞,提高了调度效率。", + "source": null, + "related": [] + }, + { + "id": "fb-008", + "type": "fill_blank", + "difficulty": 4, + "tags": [ + "java", + "aqs" + ], + "question": "Java并发包中的AQS(AbstractQueuedSynchronizer)内部维护了一个volatile int类型的______变量和一个双向链表实现的FIFO队列,ReentrantLock、Semaphore、CountDownLatch等同步器都基于AQS实现。", + "answer": [ + "state", + "state变量" + ], + "explanation": "AQS是java.util.concurrent包的核心框架,采用模板方法模式,子类通过重写tryAcquire/tryRelease(独占模式)或tryAcquireShared/tryReleaseShared(共享模式)来实现不同的同步语义。核心机制:1)state变量——volatile修饰的整数,不同同步器赋予不同含义:ReentrantLock中表示重入次数(0为未锁定),Semaphore中表示可用许可数,CountDownLatch中表示倒计数值;2)CLH队列——基于双向链表实现的FIFO等待队列,节点封装了等待线程和等待状态;3)acquire/release流程——线程获取锁失败时被封装为节点加入队列尾部并自旋或park阻塞,前驱节点释放后通过unpark唤醒后继节点。", + "source": null, + "related": [] + }, + { + "id": "fb-009", + "type": "fill_blank", + "difficulty": 4, + "tags": [ + "java", + "forkjoinpool" + ], + "question": "Java ForkJoinPool采用工作窃取算法,每个工作线程维护一个______队列(deque),当线程自己的任务队列为空时,它会从其他线程队列的尾部窃取任务执行,以此实现负载均衡。", + "answer": [ + "双端队列", + "deque", + "Deque", + "双端", + "work-stealing queue" + ], + "explanation": "ForkJoinPool是ExecutorService的特殊实现,专为分治任务(divide-and-conquer)设计。其核心设计:1)双端队列(Deque)——每个worker线程拥有自己的双端队列,本地任务从头部push/pop(LIFO,利用缓存局部性),窃取时从其他线程队列的尾部pop(减少竞争);2)工作窃取(work-stealing)——空闲线程从其他忙碌线程的队列尾部窃取任务,避免线程空闲;3)任务拆分——继承RecursiveTask(有返回值)或RecursiveAction(无返回值),通过fork()将子任务推入自己的队列,join()等待子任务结果;4)ForkJoinPool.commonPool()——JDK 8+提供默认共享池,parallelStream()底层使用它。任务粒度过细会导致调度开销过大,过粗则无法充分利用并行性。", + "source": null, + "related": [] + }, + { + "id": "fb-010", + "type": "fill_blank", + "difficulty": 5, + "tags": [ + "go", + "pprof" + ], + "question": "Go的pprof工具支持多种profile类型:CPU profile记录程序在各位置的采样频率,heap profile记录内存分配,block profile记录goroutine在同步原语上的阻塞时间,而______ profile专门记录goroutine的调用栈,用于排查goroutine泄漏问题。", + "answer": [ + "goroutine", + "goroutine profile" + ], + "explanation": "pprof是Go内置的强大性能分析工具,支持的profile类型各有用途:1)CPU profile——通过定时采样(默认100Hz)记录CPU使用热点,用于发现CPU密集型瓶颈;2)heap profile——记录堆内存的分配和持有情况,可用于定位内存泄漏和过度分配;3)block profile——记录goroutine在sync.Mutex、channel等同步原语上阻塞的时长和次数,用于发现锁竞争瓶颈;4)goroutine profile——列出所有活跃goroutine的调用栈和状态(running/waiting/sleeping等),是排查goroutine泄漏的关键工具。当goroutine泄漏时,该profile会显示大量goroutine堆积在同一个调用栈上(如等待永远不会接收到数据的channel)。定位后通常需要检查:未关闭的channel、未取消的context、死循环中缺少退出条件等问题。可通过runtime/pprof包或net/http/pprof端点(/debug/pprof/)获取profile数据,再用go tool pprof进行可视化分析或生成火焰图。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/interview-prep/go-java-concurrency/meta.json b/topics/interview-prep/go-java-concurrency/meta.json new file mode 100644 index 0000000..5ca1951 --- /dev/null +++ b/topics/interview-prep/go-java-concurrency/meta.json @@ -0,0 +1,25 @@ +{ + "slug": "go-java-concurrency", + "name": "Go/Java并发模型", + "description": "GMP调度、CSP模型、AQS/ForkJoinPool、goroutine泄漏、JMM、happens-before、pprof/JFR", + "tags": [], + "question_files": { + "single_choice": "single_choice.json", + "true_false": "true_false.json", + "fill_blank": "fill_blank.json", + "short_answer": "short_answer.json", + "code_reading": "code_reading.json" + }, + "stats": { + "total": 45, + "by_type": { + "single_choice": 15, + "true_false": 10, + "fill_blank": 10, + "short_answer": 5, + "code_reading": 5 + } + }, + "created": "2026-09-09", + "updated": "2026-09-09" +} \ No newline at end of file diff --git a/topics/interview-prep/go-java-concurrency/short_answer.json b/topics/interview-prep/go-java-concurrency/short_answer.json new file mode 100644 index 0000000..c05c61d --- /dev/null +++ b/topics/interview-prep/go-java-concurrency/short_answer.json @@ -0,0 +1,138 @@ +{ + "topic": "go-java-concurrency", + "type": "short_answer", + "schema_version": "1.0.0", + "generated": "2026-09-09T16:21:56+08:00", + "questions": [ + { + "id": "sa-001", + "type": "short_answer", + "difficulty": 3, + "tags": [ + "go", + "gmp" + ], + "question": "请简述Go语言GMP调度模型中work-stealing机制的工作原理,包括什么情况下会触发stealing,以及stealing的来源优先级。", + "answer": "在GMP模型中,每个P维护一个本地运行队列(local run queue),同时存在一个全局运行队列(global run queue)。当一个P的本地队列为空时,该P对应的M(线程)会按照以下顺序尝试获取新的G(goroutine):1)先检查全局队列,通过GOMAXPROCS/2的固定频率周期性检查全局队列防止饥饿;2)若本地队列为空且全局队列也无可用G,则通过netpoller检查网络轮询器中是否有就绪的G;3)若仍无,则从其他P的本地队列中窃取一半的G(work-stealing)。stealing的来源是随机选择另一个P,取其本地队列长度的一半G,放到自己的本地队列中运行。这一机制保证了各P之间的负载均衡,避免某个P空闲而其他P过载。", + "keywords": [ + "work-stealing", + "本地队列", + "全局队列", + "空闲P", + "负载均衡", + "netpoller" + ], + "scoring_rubric": "满分需涵盖:(1) 本地队列为空时触发stealing(2分);(2) stolen来源是其他P的本地队列且取一半G(2分);(3) 提及全局队列周期性检查防止饥饿(1分);(4) 提及netpoller作为中间检查步骤(1分)。缺少work-stealing核心描述扣2分,未提及steal数量(一半)扣1分。", + "explanation": "work-stealing是GMP模型保持负载均衡的核心机制。当一个P处理完本地所有G后,它不会立即阻塞,而是主动去其他忙碌的P那里'偷'任务。Go调度器的完整G获取顺序为:本地队列→全局队列(每61次调度检查一次)→netpoller→随机stealing。这种设计在保持调度效率的同时,通过周期性全局队列检查避免了全局队列中的G被永久饥饿。", + "source": null, + "related": [] + }, + { + "id": "sa-002", + "type": "short_answer", + "difficulty": 4, + "tags": [ + "go", + "channel", + "hchan" + ], + "question": "从底层实现角度,解释Go channel中hchan结构体的核心字段及其作用。当一个goroutine向无缓冲channel发送数据时,发生了哪些步骤?", + "answer": "hchan是channel的底层结构体,核心字段包括:1)qcount uint:队列中的元素个数;2)dataqsiz uint:环形队列的容量(缓冲区大小,无缓冲channel为0);3)buf unsafe.Pointer:指向环形队列的指针;4)sendx uint:环形队列中发送位置的索引;5)recvx uint:环形队列中接收位置的索引;6)sendq waitq:发送等待队列,存放因发送阻塞的goroutine(sudog链表);7)recvq waitq:接收等待队列,存放因接收阻塞的goroutine;8)lock mutex:保护hchan所有字段的互斥锁。\n\n无缓冲channel发送数据的步骤:1)加锁(lock(ch.lock));2)尝试直接将数据拷贝给正在等待接收的goroutine(recvq不为空时),此时不需要经过环形队列;3)若没有接收者且环形队列未满,则将数据拷贝到buf中(但无缓冲channel这步不会执行);4)若无接收者且无法入队,则当前goroutine被封装为sudog放入sendq等待队列,并挂起(gopark);5)当有goroutine接收时,被挂起的发送者被唤醒,数据直接从发送者的栈拷贝到接收者的栈。", + "keywords": [ + "hchan", + "qcount", + "buf", + "sendq", + "recvq", + "lock", + "sudog", + "gopark", + "直接拷贝" + ], + "scoring_rubric": "满分需涵盖:(1) 正确列出hchan核心字段(qcount/buf/sendx/recvx/sendq/recvq/lock)至少5个(3分);(2) 无缓冲channel发送步骤中提及直接拷贝给等待接收者(2分);(3) 无接收者时goroutine阻塞挂起(gopark)(2分);(4) 提及lock互斥锁保护(1分)。字段缺失2个以上扣1分,未说明无缓冲channel特殊性扣1分。", + "explanation": "hchan是Go channel的核心数据结构,所有channel操作都在其上加锁进行。Go对channel做了大量优化:当有goroutine在等待时,发送和接收操作会绕过环形队列,直接在goroutine栈之间拷贝数据(hand-off机制),避免了额外的内存拷贝。这解释了为什么在高并发场景下Go channel的性能优于共享内存+锁的方案。无缓冲channel的dataqsiz为0,buf为nil,所有数据交换都通过sudog等待队列和直接拷贝完成。", + "source": null, + "related": [] + }, + { + "id": "sa-003", + "type": "short_answer", + "difficulty": 4, + "tags": [ + "java", + "aqs", + "synchronized" + ], + "question": "请解释Java中AQS(AbstractQueuedSynchronizer)的核心设计思想,并说明ReentrantLock的公平锁和非公平锁在AQS层面的主要区别。", + "answer": "AQS是Java并发包(JUC)的基石,其核心设计思想是:通过一个volatile int state变量表示同步状态,配合一个FIFO的CLH(Craig, Landin, and Hagersten)队列来管理等待获取锁的线程。子类通过重写tryAcquire/tryRelease(独占模式)或tryAcquireShared/tryReleaseShared(共享模式)来实现不同的同步器。\n\nReentrantLock在AQS层面的公平锁与非公平锁区别:\n1)非公平锁(默认):线程调用lock()时,先尝试直接通过CAS修改state抢占锁,若成功直接获取锁进入临界区,不关心CLH队列中是否有等待线程。只有CAS失败时才会走正常的acquire流程(可能入队等待)。\n2)公平锁:线程调用lock()时,先检查CLH队列中是否有前驱节点在等待,若有则必须入队排队,遵循FIFO顺序获取锁。\n\n两者在tryAcquire中的关键差异:非公平锁的tryAcquire中没有hasQueuedPredecessors()检查;公平锁在发现state为0(锁空闲)时,会先调用hasQueuedPredecessors()判断是否有等待线程,若有则不抢锁,返回false让当前线程入队。", + "keywords": [ + "AQS", + "state", + "CLH队列", + "CAS", + "tryAcquire", + "hasQueuedPredecessors", + "公平锁", + "非公平锁" + ], + "scoring_rubric": "满分需涵盖:(1) AQS核心:state变量 + CLH队列(2分);(2) 子类重写tryAcquire/tryRelease(1分);(3) 非公平锁直接CAS抢占(2分);(4) 公平锁检查hasQueuedPredecessors排队(2分)。未提及CLH队列扣2分,公平/非公平区别描述不清扣2分。", + "explanation": "AQS采用模板方法模式:模板方法(acquire/acquireShared)定义了获取锁的通用流程(try成功则返回,失败则入队并park),具体逻辑由子类通过tryAcquire等钩子方法实现。CLH队列是其高效的关键:每个等待线程被封装为Node节点,通过自旋+CAS+LockSupport.park/unpark实现低延迟的线程唤醒。非公平锁吞吐量通常高于公平锁,因为减少了线程上下文切换,但可能导致线程饥饿。", + "source": null, + "related": [] + }, + { + "id": "sa-004", + "type": "short_answer", + "difficulty": 5, + "tags": [ + "java", + "jmm", + "volatile" + ], + "question": "请解释Java内存模型(JMM)中volatile关键字的两条语义规则,并举例说明在什么场景下仅靠volatile无法保证线程安全(即需要synchronized或Atomic类的原因)。", + "answer": "volatile的两条语义规则:\n1)可见性保证(写后读):对volatile变量的写操作 happens-before 后续对该变量的读操作。这意味着写入线程对volatile变量的新值以及该写操作之前(程序顺序上)对其他变量的修改,都会对读取线程可见。底层通过内存屏障(Store Barrier + Load Barrier)实现:写时插入StoreStore和StoreLoad屏障,读时插入LoadLoad和LoadStore屏障。\n2)禁止指令重排序:volatile通过内存屏障阻止编译器和CPU对volatile变量的读写操作与其前后的指令进行重排序,保证了一定的有序性。\n\n仅靠volatile无法保证线程安全的场景——复合操作(read-modify-write):\n例1:volatile int count = 0; count++不是原子操作,它包含读count、加1、写count三步。两个线程同时执行count++,可能都读到相同的旧值,导致最终结果只加了1而非2。\n例2:volatile引用的非原子更新:volatile Object ref; ref = new Object(); 虽然ref的赋值本身是安全的,但如果有检查后执行(check-then-act)模式如 if (ref == null) ref = new Object(); 仍存在竞态条件。\n\n解决方案:使用synchronized块保证复合操作的原子性,或使用AtomicInteger等基于CAS的原子类。", + "keywords": [ + "JMM", + "volatile", + "happens-before", + "内存屏障", + "可见性", + "有序性", + "原子性", + "CAS", + "复合操作" + ], + "scoring_rubric": "满分需涵盖:(1) 可见性语义:写happens-before读(2分);(2) 禁止指令重排序(2分);(3) 举例说明复合操作(如count++)非原子(2分);(4) 提及内存屏障实现原理(1分)。两条语义缺一扣2分,无反例说明扣2分,未提及解决方案扣1分。", + "explanation": "volatile解决的是可见性和有序性问题,但不解决原子性问题。JMM的happens-before八大规则中,volatile规则是:对volatile变量的写happens-before后续对同一变量的读。这意味着volatile写之前的所有操作(包括普通变量的写)都对volatile读之后的操作可见。但count++这样的操作涉及多次内存访问,volatile只能保证每次读到最新值,不能保证读-改-写这个整体是原子的。这就是为什么需要AtomicInteger(内部使用CAS+volatile)或synchronized来保护复合操作。", + "source": null, + "related": [] + }, + { + "id": "sa-005", + "type": "short_answer", + "difficulty": 5, + "tags": [ + "go", + "pprof", + "java" + ], + "question": "请分别描述如何使用Go pprof和Java JFR(Java Flight Recorder)定位goroutine泄漏问题,包括关键的分析步骤和关注的指标。", + "answer": "Go pprof定位goroutine泄漏:\n1)引入net/http/pprof包或使用runtime/pprof:在HTTP服务中导入_ \"net/http/pprof\"并启动debug/pprof端点;或在程序中通过pprof.StartCPUProfile等API采集。\n2)关键端点:访问/debug/pprof/goroutine?debug=1查看所有goroutine的完整堆栈;debug=2显示所有goroutine的原始堆栈。\n3)分析步骤:采集goroutine profile → 按堆栈分组查看数量 → 重点关注阻塞在channel收发(chansend/chanrecv)、select、time.Sleep、net.Conn.Read/Write的goroutine → 如果某个堆栈的goroutine数量持续增长,则定位到泄漏点。\n4)结合race detector(go run -race)检测数据竞争,以及使用go tool pprof分析CPU和内存profile辅助定位。\n5)关注指标:goroutine总数(runtime.NumGoroutine())、各阻塞点的goroutine分布。\n\nJava JFR定位线程泄漏:\n1)启动JFR:java -XX:StartFlightRecording=duration=60s,filename=recording.jfr 或通过jcmd pid JFR.start。\n2)关键事件:关注jdk.ThreadStart和jdk.ThreadEnd事件,可统计线程创建/销毁速率;jdk.ThreadLock事件可发现线程阻塞原因。\n3)分析步骤:用JDK Mission Control(JMC)打开.jfr文件 → 查看'线程'面板中的线程总数和线程状态分布 → 关注BLOCKED和WAITING状态的线程数是否持续增长 → 检查线程的堆栈,定位到wait()/park()/sleep()等阻塞点。\n4)关注指标:活跃线程数、BLOCKED/WAITING线程数趋势、线程池核心参数与实际使用量对比(如ThreadPoolExecutor的activeCount与corePoolSize)。", + "keywords": [ + "pprof", + "goroutine泄漏", + "debug/pprof/goroutine", + "JFR", + "JMC", + "ThreadStart", + "ThreadEnd", + "阻塞分析", + "线程状态" + ], + "scoring_rubric": "Go部分满分4分:(1) pprof接入方式(1分);(2) goroutine profile查看方法(1分);(3) 分析步骤与阻塞点定位(2分)。Java部分满分4分:(1) JFR启动方式(1分);(2) 关键事件(ThreadStart/End)(1分);(3) JMC分析步骤与线程状态关注(2分)。两个工具缺一扣4分,分析步骤不完整扣2分。", + "explanation": "goroutine泄漏是Go中常见但隐蔽的性能问题,因为goroutine创建成本低,容易被大量创建后遗忘。pprof的goroutine profile是最直接的诊断工具,debug=1模式下可看到每个goroutine的创建位置和当前阻塞原因,通过分组统计即可快速定位泄漏点。Java JFR是JDK内置的低开销(<1%性能影响)诊断工具,适合生产环境长期运行。JMC提供可视化界面,Thread面板可按线程状态分组展示,配合堆栈信息可精确定位线程泄漏或阻塞的根因。两者的核心思路一致:采集→分组统计→定位阻塞点→分析根因。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/interview-prep/go-java-concurrency/single_choice.json b/topics/interview-prep/go-java-concurrency/single_choice.json new file mode 100644 index 0000000..ea73a59 --- /dev/null +++ b/topics/interview-prep/go-java-concurrency/single_choice.json @@ -0,0 +1,332 @@ +{ + "topic": "go-java-concurrency", + "type": "single_choice", + "schema_version": "1.0.0", + "generated": "2026-09-09T16:21:56+08:00", + "questions": [ + { + "id": "sc-001", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "Go", + "GMP", + "调度模型" + ], + "question": "在 Go 的 GMP 调度模型中,P(Processor)代表什么?", + "options": { + "A": "操作系统线程(OS Thread)", + "B": "逻辑处理器,持有本地运行队列和资源上下文", + "C": "用户级轻量级线程(goroutine)", + "D": "内存管理单元(MMU)" + }, + "answer": "B", + "explanation": "P(Processor)是逻辑处理器,不是物理 CPU 核心。P 持有一个本地 goroutine 运行队列(local run queue)和执行 goroutine 所需的资源上下文(如 mcache、span 等)。G 是 goroutine,M 是操作系统线程。默认情况下 P 的数量等于 GOMAXPROCS,默认值为 CPU 核心数。P 在 G、M 之间起桥梁作用,G 绑定到 P 的本地队列上运行,M 则需要绑定一个 P 才能执行 G。", + "source": null, + "related": [] + }, + { + "id": "sc-002", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "Go", + "GMP", + "work-stealing" + ], + "question": "关于 Go GMP 调度器的 work-stealing 机制,以下描述正确的是?", + "options": { + "A": "当某个 P 的本地队列为空时,它会从其他 P 的本地队列中窃取一半的 goroutine", + "B": "当某个 M 空闲时,它会随机选择一个 P 并抢占其正在运行的 goroutine", + "C": "work-stealing 仅在全局队列为空时才会触发", + "D": "每次窃取操作都会从目标 P 的队列头部取走一个 goroutine" + }, + "answer": "A", + "explanation": "当一个 P 的本地队列为空且全局队列也为空时,调度器会执行 work-stealing:随机选择另一个 P,将其本地队列中的 goroutine 偷走一半。这种设计保证了负载均衡——不会出现某个 P 忙碌而另一个 P 空闲的情况。选项 C 错误,work-stealing 的前提是本地队列为空,然后先尝试全局队列,再尝试 work-stealing。选项 D 错误,窃取时是取走一半(约 half),而非单个。", + "source": null, + "related": [] + }, + { + "id": "sc-003", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "Go", + "GMP", + "sysmon", + "netpoller" + ], + "question": "Go 运行时中 sysmon 线程的主要职责不包括以下哪项?", + "options": { + "A": "检测并抢占长时间运行的 goroutine", + "B": "回收超过 10ms 未使用的内存堆栈", + "C": "将因系统调用阻塞的 M 上的 P 抢占并绑定到空闲 M", + "D": "编译 Go 源码中的泛型函数" + }, + "answer": "D", + "explanation": "sysmon 是一个特殊的后台 M(不绑定 P),其职责包括:1)抢占运行超过 10ms 的 goroutine(基于协作+信号的抢占式调度);2)将阻塞在系统调用上的 M 的 P 抢占出来绑定到空闲 M 上,保证 P 不被浪费;3)触发 GC 的后台标记;4)回收长时间未使用的堆栈。编译工作由编译器在编译期完成,与 sysmon 无关。netpoller 也与 sysmon 有协作关系,用于处理网络 I/O 的轮询。", + "source": null, + "related": [] + }, + { + "id": "sc-004", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "Go", + "channel", + "hchan" + ], + "question": "在 Go 中,一个无缓冲 channel(unbuffered channel)的发送操作何时完成?", + "options": { + "A": "将值放入 channel 缓冲区后立即返回", + "B": "必须有另一个 goroutine 同时执行接收操作,两者同步后才完成", + "C": "无论是否有接收方,发送操作都会立即返回", + "D": "发送操作会阻塞直到 channel 被关闭" + }, + "answer": "B", + "explanation": "无缓冲 channel(make(chan T))是同步通道,发送方在接收方准备好之前会阻塞。只有当另一个 goroutine 执行了接收操作,发送和接收才会同步完成。这就是 Go 的'通信通过共享内存'而非'共享内存通过通信'的核心体现。有缓冲 channel(make(chan T, n))在缓冲区未满时发送不会阻塞,这是两者的关键区别。", + "source": null, + "related": [] + }, + { + "id": "sc-005", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "Go", + "channel", + "hchan", + "底层结构" + ], + "question": "Go channel 的底层数据结构 hchan 中,sendx 和 recvx 字段的作用是什么?", + "options": { + "A": "记录 channel 的发送和接收总次数,用于性能统计", + "B": "环形缓冲区中下一个发送/接收位置的索引,用于实现 FIFO 语义", + "C": "记录当前正在阻塞的发送者/接收者数量", + "D": "指向 channel 类型元信息的指针偏移量" + }, + "answer": "B", + "explanation": "hchan 是 Go channel 的底层结构体。其中 buf 是一个环形缓冲区(用于有缓冲 channel),sendx 和 recvx 分别记录下一次发送和接收在 buf 中的索引位置,实现 FIFO(先进先出)语义。当 sendx 或 recvx 到达缓冲区末尾时会回绕到 0。与之相关的字段还有 sendq(等待发送的 goroutine 队列,即 sudog 链表)和 recvq(等待接收的 goroutine 队列)。", + "source": null, + "related": [] + }, + { + "id": "sc-006", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "Go", + "channel", + "close" + ], + "question": "关于 Go channel 的 close 语义,以下哪项描述是正确的?", + "options": { + "A": "对一个已关闭的 channel 再次调用 close 会 panic", + "B": "关闭一个 nil channel 会静默忽略,不会 panic", + "C": "关闭 channel 后,仍然可以向其发送数据", + "D": "关闭 channel 后,接收操作会立即阻塞" + }, + "answer": "A", + "explanation": "Go 对 channel 的关闭有严格的规则:1)对已关闭的 channel 再次 close 会触发 panic(send on closed channel 的同类保护);2)对 nil channel 调用 close 同样会 panic;3)关闭 channel 后不能再发送数据(会 panic),但可以继续接收——接收操作会返回缓冲区中的剩余数据,缓冲区读完后接收操作返回零值且 ok 为 false;4)只有发送方或唯一所有者应该关闭 channel,不应由接收方关闭。", + "source": null, + "related": [] + }, + { + "id": "sc-007", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "Go", + "sync", + "Mutex", + "RWMutex" + ], + "question": "Go 的 sync.RWMutex 相比 sync.Mutex 的主要优势是什么?", + "options": { + "A": "在写锁模式下性能更好", + "B": "允许多个 goroutine 同时持有读锁,提高读多写少场景的并发性能", + "C": "支持可重入锁,同一个 goroutine 可多次加锁", + "D": "使用无锁算法实现,完全不涉及操作系统原语" + }, + "answer": "B", + "explanation": "RWMutex 提供了读写分离的锁机制:多个 goroutine 可以同时持有 RLock(读锁),只有写锁是互斥的。这在读多写少的场景下显著优于 Mutex(所有操作互斥)。选项 A 错误,RWMutex 的写锁性能通常略低于纯 Mutex,因为需要维护读写计数。选项 C 错误,Go 的 Mutex 和 RWMutex 都不是可重入的,重复加锁会导致死锁。选项 D 错误,底层仍使用原子操作和信号量。", + "source": null, + "related": [] + }, + { + "id": "sc-008", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "Go", + "sync", + "Once", + "Pool" + ], + "question": "关于 Go 的 sync.Once 和 sync.Pool,以下哪项描述是正确的?", + "options": { + "A": "sync.Once 的 Do 方法在并发调用时,只有第一个调用会执行传入的函数,其余阻塞等待", + "B": "sync.Pool 中的对象永远不会被回收,适合存放全局唯一实例", + "C": "sync.Once 的 Do 方法中如果传入的函数 panic,后续调用会重新执行该函数", + "D": "sync.Pool 是线程安全的,但不支持跨 goroutine 共享对象" + }, + "answer": "A", + "explanation": "sync.Once 的 Do(f) 保证 f 只被执行一次,即使多个 goroutine 并发调用。第一个调用者执行 f,其他调用者阻塞等待 f 完成后直接返回。选项 B 错误,Pool 中的对象在每次 GC 时可能被清除(Go 1.13 后有 victim cache 机制,延迟一个 GC 周期),不适合存放必须持久化的对象。选项 C 错误,如果 f panic,Once 仍然认为已执行过,后续调用不会再执行 f。选项 D 错误,Pool 设计上就是跨 goroutine 共享的,用于对象复用以减少 GC 压力。", + "source": null, + "related": [] + }, + { + "id": "sc-009", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "Go", + "sync", + "Map", + "并发" + ], + "question": "Go 的 sync.Map 适合以下哪种使用场景?", + "options": { + "A": "频繁写入、少量读取的场景", + "B": "key 集合稳定,大量并发读取、极少写入的场景", + "C": "需要有序遍历所有 key-value 的场景", + "D": "需要支持批量删除操作的场景" + }, + "answer": "B", + "explanation": "sync.Map 针对两种场景优化:1)key 集合稳定后只读(read-only);2)多个 goroutine 读写不同的 key(disjoint key sets)。它内部使用 read(只读,无锁访问)和 dirty(写锁保护)两个 map 实现读写分离,在大量并发读的场景下性能远优于 Mutex+map。选项 A 错误,频繁写入会导致 read/dirty 频繁同步,性能反而不如加锁。选项 C 错误,sync.Map 不保证遍历顺序。选项 D 错误,sync.Map 不提供批量删除接口。", + "source": null, + "related": [] + }, + { + "id": "sc-010", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "Go", + "内存模型", + "happens-before", + "race" + ], + "question": "Go 的 -race 检测器主要基于什么技术实现?", + "options": { + "A": "静态代码分析,在编译期检测所有潜在的数据竞争", + "B": "AddressSanitizer(ASan),在运行时检测内存越界访问", + "C": "ThreadSanitizer(TSan),在运行时动态追踪内存访问的 happens-before 关系", + "D": "在每个内存访问处插入互斥锁,通过死锁检测间接发现竞争" + }, + "answer": "C", + "explanation": "Go 的 race detector 基于 Google 的 ThreadSanitizer(TSan)技术,在编译期插桩(instrumentation)并在运行时动态追踪所有内存访问和同步操作,构建 happens-before 关系图。如果两个并发访问(至少一个是写)之间没有 happens-before 关系,就报告数据竞争。选项 A 错误,它不是纯静态分析,需要运行时执行才能检测。选项 B 错误,ASan 检测的是内存安全(越界、use-after-free),不是数据竞争。选项 D 完全不正确。", + "source": null, + "related": [] + }, + { + "id": "sc-011", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "Go", + "goroutine泄漏", + "pprof", + "context" + ], + "question": "以下哪种情况最容易导致 goroutine 泄漏?", + "options": { + "A": "goroutine 中执行了一次短暂的 CPU 计算后正常返回", + "B": "goroutine 向一个没有接收者的无缓冲 channel 发送数据", + "C": "goroutine 调用了 time.Sleep 后正常退出", + "D": "goroutine 中使用了 defer 语句" + }, + "answer": "B", + "explanation": "goroutine 泄漏是指 goroutine 无法正常退出,持续占用内存和调度资源。向无缓冲 channel 发送数据时,如果没有接收者,发送操作会永久阻塞,goroutine 就泄漏了。常见的泄漏模式还包括:1)从无人发送的 channel 接收;2)未设置超时或取消的阻塞操作(如未用 context 控制的 HTTP 请求);3)无限循环且无退出条件。排查工具:pprof 的 goroutine profile 可以看到所有存活 goroutine 的堆栈,定位泄漏源。", + "source": null, + "related": [] + }, + { + "id": "sc-012", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "Java", + "线程", + "ThreadPoolExecutor" + ], + "question": "Java ThreadPoolExecutor 的核心参数 corePoolSize、maximumPoolSize、workQueue 之间的执行关系是?", + "options": { + "A": "任务提交后直接创建线程,不受 corePoolSize 限制", + "B": "先创建到 corePoolSize 个线程 → 放入 workQueue → 满了才创建到 maximumPoolSize 个线程", + "C": "先创建到 maximumPoolSize 个线程 → 放入 workQueue → 满了才缩减到 corePoolSize", + "D": "workQueue 仅在 corePoolSize 和 maximumPoolSize 都满时才使用" + }, + "answer": "B", + "explanation": "ThreadPoolExecutor 的任务处理流程:1)当前线程数 < corePoolSize → 直接创建新线程执行;2)线程数 ≥ corePoolSize → 放入 workQueue 等待;3)workQueue 已满且线程数 < maximumPoolSize → 创建新线程执行;4)线程数 ≥ maximumPoolSize 且队列满 → 执行拒绝策略(RejectedExecutionHandler)。keepAliveTime 控制非核心线程的空闲存活时间。", + "source": null, + "related": [] + }, + { + "id": "sc-013", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "Java", + "锁机制", + "synchronized", + "锁升级" + ], + "question": "Java synchronized 的锁升级过程是怎样的?", + "options": { + "A": "无锁 → 轻量级锁 → 偏向锁 → 重量级锁", + "B": "偏向锁 → 轻量级锁 → 重量级锁(不可逆)", + "C": "无锁 → 偏向锁 → 轻量级锁 → 重量级锁", + "D": "轻量级锁 → 重量级锁 → 偏向锁(竞争结束后降级)" + }, + "answer": "C", + "explanation": "JVM 对 synchronized 的优化采用逐步升级策略:1)无锁状态 → 首次进入同步块时,如果对象头 Mark Word 未偏向任何线程,升级为偏向锁(仅记录线程 ID,无 CAS 开销);2)当第二个线程尝试竞争时,偏向锁撤销,升级为轻量级锁(通过 CAS 将 Mark Word 替换为指向栈帧中 Lock Record 的指针);3)轻量级锁竞争失败(自旋超过一定次数),膨胀为重量级锁(OS mutex,未获锁的线程阻塞挂起)。注意:偏向锁在 JDK 15 后默认关闭,因为其撤销成本较高。", + "source": null, + "related": [] + }, + { + "id": "sc-014", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "Java", + "JMM", + "volatile", + "happens-before" + ], + "question": "在 Java 内存模型(JMM)中,volatile 变量的 happens-before 语义是?", + "options": { + "A": "对 volatile 变量的写操作 happens-before 后续对同一变量的读操作", + "B": "volatile 仅保证变量的原子性,不保证可见性", + "C": "volatile 读写操作不会插入内存屏障", + "D": "volatile 变量的写 happens-before 所有后续的读写操作" + }, + "answer": "A", + "explanation": "根据 JMM 的 happens-before 八大规则之一:volatile 变量规则——对 volatile 字段的写操作 happens-before 后续对同一字段的读操作。具体实现上,volatile 写后插入 StoreStore + StoreLoad 屏障,volatile 读前插入 LoadLoad + LoadStore 屏障。选项 B 错误,volatile 不保证原子性(如 i++ 不是原子操作),但保证可见性。选项 C 错误,volatile 正是通过内存屏障实现的。选项 D 错误,happens-before 是针对同一变量的特定读写对,不是全局的。", + "source": null, + "related": [] + }, + { + "id": "sc-015", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "Java", + "ForkJoinPool", + "工作窃取" + ], + "question": "Java ForkJoinPool 的工作窃取(work-stealing)机制是指?", + "options": { + "A": "空闲线程从全局任务队列中取任务执行", + "B": "空闲线程从其他忙碌线程的本地任务队列尾部窃取任务执行", + "C": "主线程将任务平均分配给所有工作线程", + "D": "工作线程按固定顺序依次执行所有提交的任务" + }, + "answer": "B", + "explanation": "ForkJoinPool 采用工作窃取算法:每个工作线程维护自己的双端队列(deque),从头部取任务执行(FIFO);当自己的队列为空时,从其他忙碌线程的队列尾部窃取任务(LIFO),减少竞争。这种设计比传统的线程池(单个共享队列)更高效,因为减少了锁争用。ForkJoinPool 是 parallelStream() 的底层线程池,commonPool 是共享的 ForkJoinPool 实例。RecursiveTask(有返回值)和 RecursiveAction(无返回值)是 fork/join 编程模型的任务基类。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/interview-prep/go-java-concurrency/true_false.json b/topics/interview-prep/go-java-concurrency/true_false.json new file mode 100644 index 0000000..8163892 --- /dev/null +++ b/topics/interview-prep/go-java-concurrency/true_false.json @@ -0,0 +1,152 @@ +{ + "topic": "go-java-concurrency", + "type": "true_false", + "schema_version": "1.0.0", + "generated": "2026-09-09T16:21:56+08:00", + "questions": [ + { + "id": "tf-001", + "type": "true_false", + "difficulty": 2, + "tags": [ + "go", + "channel" + ], + "question": "在Go中,向已关闭的channel发送数据会导致panic,但从已关闭的channel接收数据会立即返回零值且ok为false。", + "answer": true, + "explanation": "Go中channel的close语义明确规定:向已关闭的channel发送数据会触发panic(send on closed channel),而从已关闭的channel接收数据不会阻塞,而是返回该元素类型的零值,第二个返回值ok为false。这是channel安全关闭的核心规则——关闭者负责发送端的停止,接收者只需检查ok值。", + "source": null, + "related": [] + }, + { + "id": "tf-002", + "type": "true_false", + "difficulty": 3, + "tags": [ + "go", + "goroutine", + "pprof" + ], + "question": "Go中goroutine泄漏最常见的原因之一是向无缓冲channel发送数据后,没有对应的接收者,导致发送方goroutine永远阻塞。", + "answer": true, + "explanation": "goroutine泄漏的本质是goroutine无法正常退出。向无缓冲channel发送时,如果没有goroutine在接收,发送方会永久阻塞。类似地,从无人发送的channel接收也会阻塞。其他常见泄漏场景包括:context未正确取消导致的无限等待循环、select中没有default分支且所有case都不就绪等。可以通过pprof的goroutine profile(runtime.NumGoroutine()或net/http/pprof)来定位泄漏的goroutine及其阻塞堆栈。", + "source": null, + "related": [] + }, + { + "id": "tf-003", + "type": "true_false", + "difficulty": 3, + "tags": [ + "go", + "gmp" + ], + "question": "Go的GMP调度模型中,当一个M(系统线程)发现本地P的本地队列为空时,会先尝试从全局队列获取G,再尝试从其他P的本地队列偷取(work-stealing)一半的G。", + "answer": true, + "explanation": "GMP调度模型中,M绑定P后优先从P的本地队列获取G执行。当本地队列为空时,调度器的窃取逻辑为:1)先检查全局队列(global run queue);2)再尝试netpoller(网络轮询器);3)最后从其他P的本地队列偷取(随机选择一个P,偷取其本地队列一半的G)。sysmon监控线程会定期检测长时间运行的G(抢占式调度),并将空闲P绑定到空闲M。", + "source": null, + "related": [] + }, + { + "id": "tf-004", + "type": "true_false", + "difficulty": 4, + "tags": [ + "java", + "jmm", + "volatile" + ], + "question": "在Java内存模型(JMM)中,volatile变量的写操作对后续所有线程的读操作都可见,因此volatile可以完全替代synchronized来保证复合操作的原子性。", + "answer": false, + "explanation": "volatile确实保证了可见性(写入后立即刷新到主内存,读取时从主内存加载)和有序性(禁止指令重排序),但它不保证复合操作的原子性。例如volatile int count; count++实质是读-改-写三步操作,多个线程并发执行时仍会出现竞态条件。对于复合操作的原子性,需要使用synchronized、ReentrantLock或java.util.concurrent.atomic包中的原子类(如AtomicInteger的CAS操作)。volatile的经典适用场景是状态标志位(boolean flag)和双重检查锁定(DCL)单例模式。", + "source": null, + "related": [] + }, + { + "id": "tf-005", + "type": "true_false", + "difficulty": 3, + "tags": [ + "java", + "aqs", + "synchronized" + ], + "question": "Java中AQS(AbstractQueuedSynchronizer)是ReentrantLock和Semaphore等同步器的核心实现基础,它通过CLH队列变体和volatile状态变量来实现线程排队与唤醒。", + "answer": true, + "explanation": "AQS是java.util.concurrent包的基石,其核心机制为:1)内部维护一个volatile int state变量表示同步状态;2)使用CLH(Craig, Landin, and Hagersten)队列变体实现线程排队,每个等待线程被封装为Node节点;3)获取锁失败的线程被包装为Node加入CLH队列尾部,然后通过LockSupport.park()阻塞;4)释放锁时通过unparkSuccessor()唤醒队列中的下一个线程。ReentrantLock通过state表示重入次数,Semaphore通过state表示可用许可数,CountDownLatch通过state表示倒计数值。而synchronized从JVM层面实现,不依赖AQS。", + "source": null, + "related": [] + }, + { + "id": "tf-006", + "type": "true_false", + "difficulty": 2, + "tags": [ + "go", + "sync" + ], + "question": "Go的sync.Once保证传入的函数只被执行一次,即使多个goroutine同时调用Do方法,也只有一个goroutine会执行该函数,其余goroutine会阻塞等待其完成。", + "answer": true, + "explanation": "sync.Once内部通过atomic.LoadInt32/StoreInt32检查并设置标志位来保证函数只执行一次。其Do方法的实现逻辑为:先通过原子操作检查done标志,若已执行则直接返回;若未执行,则通过Mutex加锁后再次检查(double-check)并执行函数。其他并发调用Do的goroutine会在Mutex上阻塞,直到执行的goroutine完成函数调用并释放锁。这保证了初始化代码的线程安全执行,是Go中实现懒初始化单例的标准方式。", + "source": null, + "related": [] + }, + { + "id": "tf-007", + "type": "true_false", + "difficulty": 4, + "tags": [ + "java", + "forkjoinpool" + ], + "question": "Java的ForkJoinPool采用工作窃取(work-stealing)算法,空闲的工作线程会从其他繁忙线程的任务队列尾部偷取任务执行,commonPool使用ForkJoinPool.defaultParallelism()作为默认并行度。", + "answer": true, + "explanation": "ForkJoinPool的核心设计是工作窃取:每个工作线程维护一个双端队列(deque),新任务从队列头部推入(push),本线程也从头部取出执行(LIFO,有利于局部性)。当某个线程的队列为空时,它会随机选择其他线程的队列,从其尾部窃取任务(FIFO,减少竞争)。commonPool是静态共享池,通过ForkJoinPool.commonPool()获取,默认并行度Runtime.getRuntime().availableProcessors()-1(当大于1时),适用于ParallelStream和CompletableFuture等场景。RecursiveTask(有返回值)和RecursiveAction(无返回值)是ForkJoinTask的两个子类,用于定义可分解的递归任务。", + "source": null, + "related": [] + }, + { + "id": "tf-008", + "type": "true_false", + "difficulty": 4, + "tags": [ + "jmm", + "happens-before", + "go" + ], + "question": "Go的内存模型中,goroutine的创建(go关键字)发生在该goroutine的执行之前,而channel的每次发送操作都happens-before对应的接收操作完成。", + "answer": false, + "explanation": "Go内存模型的happens-before规则中:1)goroutine的创建确实happens-before该goroutine的执行开始——这是正确的。2)但对于无缓冲channel:发送操作happens-before对应的接收操作完成;而对于有缓冲channel:第k次发送happens-before第k次接收的完成。关键区别在于'完成'——接收操作的完成是指接收到了数据,而不是仅仅开始接收。该题将两种channel混为一谈,表述不够准确,且channel的发送happens-before的是'对应的接收完成'而非'接收操作本身',存在语义偏差,因此判定为false。", + "source": null, + "related": [] + }, + { + "id": "tf-009", + "type": "true_false", + "difficulty": 3, + "tags": [ + "java", + "thread" + ], + "question": "Java中通过ThreadPoolExecutor创建线程池时,核心参数maximumPoolSize必须大于等于corePoolSize,当核心线程数未满时新任务会创建核心线程执行,核心线程满后任务会先进入工作队列,队列满后才创建非核心线程。", + "answer": true, + "explanation": "ThreadPoolExecutor的任务提交流程严格遵循以下顺序:1)当前线程数 < corePoolSize → 创建新核心线程执行任务;2)核心线程满 → 将任务放入workQueue(工作队列)等待;3)队列已满且线程数 < maximumPoolSize → 创建非核心线程执行任务;4)线程数已达maximumPoolSize且队列满 → 执行拒绝策略(AbortPolicy/CallerRunsPolicy/DiscardPolicy/DiscardOldestPolicy)。maximumPoolSize < corePoolSize在语义上不成立,构造时虽然不会抛异常,但实际线程数永远不会超过corePoolSize。注意:如果使用无界队列(如LinkedBlockingQueue),maximumPoolSize实际上不会生效。", + "source": null, + "related": [] + }, + { + "id": "tf-010", + "type": "true_false", + "difficulty": 2, + "tags": [ + "java", + "jfr" + ], + "question": "Java Flight Recorder(JFR)是JDK内置的低开销性能分析工具,通过事件采样的方式记录CPU、内存、I/O、线程等运行时信息,可通过jcmd或jfr命令行工具启动录制,也可在代码中通过Recording API编程控制。", + "answer": true, + "explanation": "JFR是Oracle JDK和OpenJDK(JEP 171/328/336)内置的生产级监控框架,设计目标是开销低于1%。其核心特点包括:1)基于事件驱动的采样机制,预定义了数百种事件类型(如jdk.CPULoad、jdk.GarbageCollection、jdk.ThreadStart等);2)支持命令行启动(jcmd JFR.start、jfr start/stop/dump)和编程方式(new Recording().start());3)录制数据以.jfr文件存储,可通过JDK Mission Control(JMC)可视化分析,生成火焰图等;4)从JDK 11起可免费用于生产环境(此前商业特性)。JFR与异步采样profiling配合,能有效定位CPU热点、锁竞争、GC停顿等问题。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/interview-prep/index.json b/topics/interview-prep/index.json new file mode 100644 index 0000000..a004721 --- /dev/null +++ b/topics/interview-prep/index.json @@ -0,0 +1,103 @@ +{ + "slug": "interview-prep", + "name": "面试准备", + "description": "面试第一轮系统性复习:覆盖语言并发模型、分布式微服务、数据库进阶、消息队列、K8s与可观测性、AI工程实践", + "subtopics": [ + { + "slug": "distributed-microservice", + "name": "分布式微服务架构", + "description": "服务注册与发现、负载均衡、限流熔断、配置中心、幂等控制、分布式锁、分布式事务", + "path": "topics/interview-prep/distributed-microservice", + "stats": { + "total": 45, + "by_type": { + "single_choice": 15, + "true_false": 10, + "fill_blank": 10, + "short_answer": 5, + "code_reading": 5 + } + } + }, + { + "slug": "message-queue", + "name": "消息队列", + "description": "主流中间件选型、路由、持久化原理、事务、死信队列、推拉模式、集群化部署", + "path": "topics/interview-prep/message-queue", + "stats": { + "total": 45, + "by_type": { + "single_choice": 15, + "true_false": 10, + "fill_blank": 10, + "short_answer": 5, + "code_reading": 5 + } + } + }, + { + "slug": "k8s-observability", + "name": "K8s与可观测性", + "description": "K8s应用发布、弹性扩缩容、灰度发布、健康检查、可观测体系建设", + "path": "topics/interview-prep/k8s-observability", + "stats": { + "total": 45, + "by_type": { + "single_choice": 15, + "true_false": 10, + "fill_blank": 10, + "short_answer": 5, + "code_reading": 5 + } + } + }, + { + "slug": "go-java-concurrency", + "name": "Go/Java并发模型", + "description": "GMP调度、CSP模型、AQS/ForkJoinPool、goroutine泄漏、JMM、happens-before、pprof/JFR", + "path": "topics/interview-prep/go-java-concurrency", + "stats": { + "total": 45, + "by_type": { + "single_choice": 15, + "true_false": 10, + "fill_blank": 10, + "short_answer": 5, + "code_reading": 5 + } + } + }, + { + "slug": "database-advanced", + "name": "数据库进阶", + "description": "B+树索引优化、EXPLAIN执行计划、Redis集群/哨兵、混合持久化、慢查询优化", + "path": "topics/interview-prep/database-advanced", + "stats": { + "total": 35, + "by_type": { + "single_choice": 10, + "true_false": 5, + "fill_blank": 10, + "short_answer": 5, + "code_reading": 5 + } + } + }, + { + "slug": "ai-engineering", + "name": "AI工程实践", + "description": "Context Engineer、Harness Engineer、智能体架构、MCP协议、权限治理、可插拔配置", + "path": "topics/interview-prep/ai-engineering", + "stats": { + "total": 35, + "by_type": { + "single_choice": 10, + "true_false": 5, + "fill_blank": 10, + "short_answer": 5, + "code_reading": 5 + } + } + } + ] +} \ No newline at end of file diff --git a/topics/interview-prep/k8s-observability/code_reading.json b/topics/interview-prep/k8s-observability/code_reading.json new file mode 100644 index 0000000..3e3f506 --- /dev/null +++ b/topics/interview-prep/k8s-observability/code_reading.json @@ -0,0 +1,309 @@ +{ + "topic": "k8s-observability", + "type": "code_reading", + "schema_version": "1.0.0", + "generated": "2026-09-09T16:10:00+08:00", + "questions": [ + { + "id": "cr-001", + "type": "code_reading", + "difficulty": 3, + "tags": [ + "kubernetes", + "deployment", + "rolling-update", + "liveness", + "readiness" + ], + "question": "以下是一个微服务的 Deployment YAML 配置,阅读后回答问题。", + "code": "apiVersion: apps/v1\nkind: Deployment\nmetadata:\n name: order-service\n namespace: production\n labels:\n app: order-service\n version: v2\nspec:\n replicas: 4\n strategy:\n type: RollingUpdate\n rollingUpdate:\n maxSurge: 1\n maxUnavailable: 0\n selector:\n matchLabels:\n app: order-service\n template:\n metadata:\n labels:\n app: order-service\n version: v2\n spec:\n containers:\n - name: order\n image: registry.example.com/order-service:2.3.1\n ports:\n - containerPort: 8080\n resources:\n requests:\n cpu: 200m\n memory: 256Mi\n limits:\n cpu: 1000m\n memory: 512Mi\n livenessProbe:\n httpGet:\n path: /health/live\n port: 8080\n initialDelaySeconds: 15\n periodSeconds: 10\n failureThreshold: 3\n readinessProbe:\n httpGet:\n path: /health/ready\n port: 8080\n initialDelaySeconds: 5\n periodSeconds: 5\n failureThreshold: 2\n env:\n - name: DB_HOST\n valueFrom:\n configMapKeyRef:\n name: order-config\n key: db_host\n terminationGracePeriodSeconds: 30", + "language": "yaml", + "explanation": "本题考察 K8s Deployment 的核心配置,包括滚动更新策略、资源限制、健康检查探针等关键概念。需要理解 maxSurge/maxUnavailable 的语义以及 liveness 与 readiness 探针的区别。", + "source": null, + "related": [], + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "当前 Deployment 配置了 maxSurge=1、maxUnavailable=0 的滚动更新策略。在执行滚动更新时,Kubernetes 会先做什么?", + "options": { + "A": "先终止一个旧 Pod,再创建一个新 Pod", + "B": "先创建一个新 Pod(达到 replicas+1=5),待其就绪后再逐步替换旧 Pod", + "C": "同时终止所有旧 Pod 并创建所有新 Pod", + "D": "创建两个新 Pod,然后再终止两个旧 Pod" + }, + "answer": "B", + "explanation": "maxSurge=1 表示滚动更新过程中最多可以比期望副本数多出 1 个 Pod(即总共 5 个 Pod)。maxUnavailable=0 表示更新期间不允许任何 Pod 不可用。因此 K8s 会先创建一个新 Pod,等其通过 readinessProbe 变为 Ready 后,才终止一个旧 Pod,如此循环直到所有 Pod 更新完毕。这种策略保证了零停机滚动更新。" + }, + { + "index": 2, + "type": "single_choice", + "question": "如果 order-service 的新版本 Pod 因启动时间较长,livenessProbe 在 initialDelaySeconds=15 后开始探测,探测 3 次失败后 Pod 会被重启。此时 readinessProbe 的 initialDelaySeconds=5 和 periodSeconds=5 意味着什么?", + "options": { + "A": "readinessProbe 不会影响 Pod 的重启", + "B": "readinessProbe 在 Pod 启动 5 秒后开始,每 5 秒探测一次,连续 2 次失败则 Pod 被标记为 NotReady 并从 Service 端点移除", + "C": "readinessProbe 只在首次探测失败后才每 5 秒探测一次", + "D": "readinessProbe 和 livenessProbe 的失败次数阈值相同时行为一致" + }, + "answer": "B", + "explanation": "readinessProbe 的 initialDelaySeconds=5 表示 Pod 启动后 5 秒开始探测,periodSeconds=5 表示每 5 秒探测一次,failureThreshold=2 表示连续 2 次失败后 Pod 会被标记为 NotReady。NotReady 的 Pod 不会接收来自 Service 的流量(从 Endpoint 列表中移除),但不会被重启。这与 livenessProbe(失败后重启 Pod)的语义完全不同。readinessProbe 确保只有真正准备好接收请求的 Pod 才会被分发流量。" + }, + { + "index": 3, + "type": "short_answer", + "question": "该 Deployment 的 resources 配置中,requests 和 limits 的 CPU 值分别是多少?如果某个节点上已有其他 Pod 占用了大部分 CPU,新的 order-service Pod 可能出现什么问题?", + "answer": "requests.cpu=200m, limits.cpu=1000m。如果节点 CPU 资源不足,新 Pod 可能处于 Pending 状态无法调度;即使调度成功,在 CPU 使用量接近 1000m 时会被内核限流(throttle),导致延迟升高。", + "keywords": [ + "200m", + "1000m", + "Pending", + "throttle", + "CPU 限流" + ], + "scoring_rubric": "答出 requests.cpu=200m(1分)、limits.cpu=1000m(1分)、节点资源不足导致 Pending(1分)、CPU throttle 现象(1分),共4分。", + "explanation": "requests.cpu=200m 表示 Pod 启动时至少需要 0.2 个 CPU 核心的保证资源,limits.cpu=1000m 表示最多可使用 1 个 CPU 核心。当节点上可分配的 CPU 资源少于 200m 时,新 Pod 无法被调度(Pending)。即使调度成功,当 Pod 的 CPU 使用接近 limits 时,Linux CFS 调度器会对进程进行 throttle(限流),导致请求处理延迟增加。requests 用于调度决策,limits 用于运行时资源上限控制。" + } + ] + }, + { + "id": "cr-002", + "type": "code_reading", + "difficulty": 3, + "tags": [ + "kubernetes", + "HPA", + "pod", + "deployment" + ], + "question": "以下是一个 Horizontal Pod Autoscaler (HPA) 的配置,阅读后回答问题。", + "code": "apiVersion: autoscaling/v2\nkind: HorizontalPodAutoscaler\nmetadata:\n name: order-service-hpa\n namespace: production\nspec:\n scaleTargetRef:\n apiVersion: apps/v1\n kind: Deployment\n name: order-service\n minReplicas: 3\n maxReplicas: 20\n metrics:\n - type: Resource\n resource:\n name: cpu\n target:\n type: Utilization\n averageUtilization: 65\n - type: Resource\n resource:\n name: memory\n target:\n type: Utilization\n averageUtilization: 80\n behavior:\n scaleUp:\n stabilizationWindowSeconds: 60\n policies:\n - type: Pods\n value: 4\n periodSeconds: 60\n scaleDown:\n stabilizationWindowSeconds: 300\n policies:\n - type: Percent\n value: 10\n periodSeconds: 60", + "language": "yaml", + "explanation": "本题考察 HPA 的配置细节,包括多指标扩缩容、behavior 策略(稳定窗口、扩缩速度控制)等高级特性。需要理解 HPA 如何综合多个指标进行决策。", + "source": null, + "related": [], + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "该 HPA 配置了 CPU 平均利用率 65% 和内存平均利用率 80% 两个指标。当 CPU 利用率升至 75% 但内存利用率仅 40% 时,HPA 会如何决策?", + "options": { + "A": "不扩容,因为内存利用率未达到 80% 的阈值", + "B": "扩容,HPA 取各指标所需副本数的最大值", + "C": "扩容,HPA 取各指标所需副本数的平均值", + "D": "扩容,HPA 取各指标所需副本数的最小值" + }, + "answer": "B", + "explanation": "HPA 使用多个指标时,会分别计算每个指标所期望的副本数,然后取所有指标中需要副本数最多的那个作为最终目标副本数(取最大值)。CPU 利用率 75% 超过 65% 的目标值,HPA 会计算出一个更大的副本数;即使内存利用率低于目标,HPA 仍会以 CPU 指标计算出的较大值为准进行扩容。这种保守策略确保所有指标都在目标范围内。" + }, + { + "index": 2, + "type": "single_choice", + "question": "关于 behavior 配置,scaleDown 的 stabilizationWindowSeconds=300 和 policies 中 value=10 的组合意味着什么?", + "options": { + "A": "缩容时立即缩减 10 个 Pod", + "B": "在 300 秒稳定窗口内,如果指标持续低于目标,每 60 秒最多缩减当前副本数的 10%", + "C": "缩容速度比扩容快 5 倍", + "D": "稳定窗口期间不允许任何缩容操作" + }, + "answer": "B", + "explanation": "scaleDown 的 stabilizationWindowSeconds=300 表示 HPA 在决定缩容时,会回看过去 300 秒内的最小推荐副本数,只有当当前目标副本数低于这个最小值时才会执行缩容。policies 中 type=Percent、value=10、periodSeconds=60 表示在 60 秒的时间窗口内,最多缩减当前副本数的 10%。例如当前有 10 个 Pod,每分钟最多缩 1 个。这种机制避免了指标短暂波动导致的频繁缩容。" + }, + { + "index": 3, + "type": "short_answer", + "question": "该 HPA 的 scaleUp 和 scaleDown 配置在稳定窗口和策略上有什么不对称设计?这种设计的目的是什么?", + "answer": "scaleUp 稳定窗口 60 秒、每分钟最多增 4 个 Pod(较快);scaleDown 稳定窗口 300 秒、每分钟最多缩 10%(较慢)。目的是快速响应流量高峰、缓慢收缩以避免缩容过快导致的服务抖动。", + "keywords": [ + "60秒", + "300秒", + "快速扩容", + "缓慢缩容", + "抖动", + "稳定性" + ], + "scoring_rubric": "答出 scaleUp 更激进(1分)、scaleDown 更保守(1分)、具体数值对比(1分)、解释快扩慢缩的工程目的(1分),共4分。", + "explanation": "这是一个典型的「快扩慢缩」设计模式。scaleUp 窗口短(60s)、允许单次增加较多 Pod(最多 4 个),以便快速应对流量突增;scaleDown 窗口长(300s)、单次缩减比例小(10%),防止因指标短暂波动或流量临时下降而过度缩容导致服务不可用。在生产环境中,缩容通常比扩容更危险——缩太快可能导致请求超载或丢失连接,因此 HPA 默认和最佳实践都推荐对缩容采取更保守的策略。" + } + ] + }, + { + "id": "cr-003", + "type": "code_reading", + "difficulty": 2, + "tags": [ + "kubernetes", + "service", + "ingress" + ], + "question": "以下是微服务的 Service 和 Ingress 配置,阅读后回答问题。", + "code": "apiVersion: v1\nkind: Service\nmetadata:\n name: order-service\n namespace: production\n labels:\n app: order-service\nspec:\n type: ClusterIP\n ports:\n - port: 80\n targetPort: 8080\n protocol: TCP\n selector:\n app: order-service\n---\napiVersion: networking.k8s.io/v1\nkind: Ingress\nmetadata:\n name: api-gateway\n namespace: production\n annotations:\n nginx.ingress.kubernetes.io/ssl-redirect: \"true\"\n nginx.ingress.kubernetes.io/proxy-body-size: \"10m\"\nspec:\n ingressClassName: nginx\n tls:\n - hosts:\n - api.example.com\n secretName: api-tls-secret\n rules:\n - host: api.example.com\n http:\n paths:\n - path: /api/v1/orders\n pathType: Prefix\n backend:\n service:\n name: order-service\n port:\n number: 80\n - path: /api/v1/products\n pathType: Prefix\n backend:\n service:\n name: product-service\n port:\n number: 80", + "language": "yaml", + "explanation": "本题考察 K8s Service 与 Ingress 的配置和路由规则。需要理解 ClusterIP Service 的作用、Ingress 的路径路由机制以及 TLS 终止等概念。", + "source": null, + "related": [], + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "Service 的 targetPort=8080 而 port=80,这意味着什么?如果客户端通过 kubectl exec 进入集群内任意 Pod 并 curl order-service:80,请求会被转发到哪里?", + "options": { + "A": "请求到达 order-service Pod 的 80 端口", + "B": "请求到达 order-service Pod 的 8080 端口", + "C": "请求会被拒绝,因为集群内不能用 Service 名称访问", + "D": "请求到达 Ingress controller 的 80 端口" + }, + "answer": "B", + "explanation": "port=80 是 Service 暴露给集群内部的端口,targetPort=8080 是后端 Pod 实际监听的端口。当集群内的客户端访问 order-service:80 时,kube-proxy 会将流量转发到 Pod 的 8080 端口。这就是 Service 的端口映射机制——通过统一的 Service 端口访问不同后端容器端口的应用。" + }, + { + "index": 2, + "type": "single_choice", + "question": "用户通过浏览器访问 https://api.example.com/api/v1/orders/123,该请求的完整转发链路是什么?", + "options": { + "A": "浏览器 → Ingress (443) → order-service ClusterIP (80) → Pod (8080)", + "B": "浏览器 → Ingress (80) → order-service ClusterIP (80) → Pod (8080)", + "C": "浏览器 → Ingress (443) → Pod (8080),跳过了 Service 层", + "D": "浏览器 → Ingress (443) → product-service ClusterIP (80) → Pod (8080)" + }, + "answer": "A", + "explanation": "请求链路为:浏览器发起 HTTPS 请求 → Ingress Controller 监听 443 端口并进行 TLS 终止(解密后变为 HTTP)→ 根据 path=/api/v1/orders 和 host=api.example.com 匹配路由规则 → 转发到 order-service ClusterIP Service 的 80 端口 → kube-proxy 将流量转发到 Pod 的 8080 端口。注意 TLS 终止发生在 Ingress 层,Ingress 到 Service 之间是明文 HTTP。" + }, + { + "index": 3, + "type": "single_choice", + "question": "如果此时访问 https://api.example.com/api/v1/orders 会得到什么结果?(假设 order-service Pod 正常运行)", + "options": { + "A": "返回 404,因为路径 /api/v1/orders 不在配置中", + "B": "成功到达 order-service,因为 pathType: Prefix 会匹配以 /api/v1/orders 开头的所有路径", + "C": "返回 403 Forbidden", + "D": "请求被路由到 product-service" + }, + "answer": "B", + "explanation": "pathType: Prefix 表示前缀匹配,/api/v1/orders 会匹配所有以该路径开头的请求,包括 /api/v1/orders、/api/v1/orders/123、/api/v1/orders/search 等。如果使用 pathType: Exact,则只有完全等于 /api/v1/orders 的请求才会被匹配。在生产环境中,需要根据 API 设计选择合适的 pathType 以避免路由冲突。" + } + ] + }, + { + "id": "cr-004", + "type": "code_reading", + "difficulty": 3, + "tags": [ + "prometheus", + "grafana", + "kubernetes", + "service" + ], + "question": "以下是一组 Prometheus 监控配置,包括 ServiceMonitor 和告警规则,阅读后回答问题。", + "code": "apiVersion: monitoring.coreos.com/v1\nkind: ServiceMonitor\nmetadata:\n name: order-service-monitor\n namespace: production\n labels:\n release: prometheus\nspec:\n selector:\n matchLabels:\n app: order-service\n endpoints:\n - port: metrics\n interval: 30s\n path: /metrics\n namespaceSelector:\n matchNames:\n - production\n---\ngroups:\n- name: order-service-alerts\n rules:\n - alert: HighErrorRate\n expr: |\n sum(rate(http_requests_total{app=\"order-service\", status=~\"5..\"}[5m]))\n /\n sum(rate(http_requests_total{app=\"order-service\"}[5m]))\n > 0.05\n for: 3m\n labels:\n severity: critical\n annotations:\n summary: \"High 5xx error rate on order-service\"\n description: \"Error rate is {{ $value | humanizePercentage }} over the last 5 minutes\"\n - alert: HighLatency\n expr: |\n histogram_quantile(0.99,\n sum(rate(http_request_duration_seconds_bucket{app=\"order-service\"}[5m])) by (le)\n ) > 2\n for: 5m\n labels:\n severity: warning\n annotations:\n summary: \"P99 latency exceeding 2s on order-service\"\n - alert: PodRestartsFrequent\n expr: |\n increase(kube_pod_container_status_restarts_total{namespace=\"production\", container=\"order\"}[1h]) > 3\n for: 0m\n labels:\n severity: critical\n annotations:\n summary: \"Pod {{ $labels.pod }} restarting frequently\"\n\"", + "language": "yaml", + "explanation": "本题考察 Prometheus 监控体系的配置,包括 ServiceMonitor 的自动发现机制和 PromQL 告警规则。需要理解 PromQL 的聚合函数、直方图百分位计算以及告警条件的含义。", + "source": null, + "related": [], + "sub_questions": [ + { + "index": 1, + "type": "short_answer", + "question": "HighErrorRate 告警规则中的 PromQL 表达式计算的是什么指标?当该值大于 0.05 且持续 3 分钟时,说明什么问题?", + "answer": "该表达式计算的是 HTTP 5xx 错误率(5xx 请求数 / 总请求数)。大于 0.05 意味着超过 5% 的请求返回了服务器错误,持续 3 分钟说明这不是瞬时毛刺而是持续性问题,触发 critical 级别告警。", + "keywords": [ + "5xx 错误率", + "5%", + "HTTP 5xx", + "持续性", + "critical" + ], + "scoring_rubric": "答出错误率含义(1分)、5% 阈值(1分)、持续时间的过滤意义(1分),共3分。", + "explanation": "sum(rate(http_requests_total{status=~\"5..\"}[5m])) 计算 5 分钟内 5xx 状态码的请求速率,除以总请求速率得到错误率。rate() 使用 5 分钟窗口平滑瞬时波动。for: 3m 表示条件必须连续满足 3 分钟才触发告警,过滤掉短暂的网络抖动或重启导致的瞬时错误。如果错误率持续 >5%,通常意味着代码 bug、依赖服务故障或资源不足。" + }, + { + "index": 2, + "type": "single_choice", + "question": "HighLatency 告警使用了 histogram_quantile(0.99, ...) 表达式,这表示什么含义?如果 P99 延迟超过 2 秒,说明什么?", + "options": { + "A": "所有请求中有 1% 的请求延迟超过 2 秒", + "B": "所有请求中有 99% 的请求延迟超过 2 秒", + "C": "平均延迟超过 2 秒", + "D": "最大延迟超过 2 秒" + }, + "answer": "A", + "explanation": "histogram_quantile(0.99, ...) 计算的是第 99 百分位延迟值,即 99% 的请求延迟低于该值,只有 1% 的请求延迟超过该值。当 P99 > 2 秒时,意味着长尾延迟较高——虽然大多数请求响应正常,但有 1% 的请求耗时超过 2 秒。这在用户体验上可能表现为部分用户遇到明显卡顿。P99 是比平均值更能反映尾部延迟的关键指标。" + }, + { + "index": 3, + "type": "single_choice", + "question": "PodRestartsFrequent 告警使用 kube_pod_container_status_restarts_total 指标配合 increase 函数。以下哪个说法正确?", + "options": { + "A": "restarts_total 是 Counter 类型,increase 统计过去 1 小时内重启次数增量", + "B": "restarts_total 是 Gauge 类型,increase 统计过去 1 小时的最大值", + "C": "restarts_total 是 Histogram 类型,increase 统计分布变化", + "D": "increase 函数只能用于计算 CPU 使用量" + }, + "answer": "A", + "explanation": "kube_pod_container_status_restarts_total 是 Kubernetes 暴露的 Counter 类型指标,值只会递增不会递减。increase(kube_pod_container_status_restarts_total[1h]) > 3 表示过去 1 小时内该容器重启次数超过 3 次。for: 0m 表示一旦满足条件立即触发告警(不需要等待持续评估)。频繁重启通常是 livenessProbe 失败、OOMKilled 或应用崩溃的信号,需要立即关注。" + } + ] + }, + { + "id": "cr-005", + "type": "code_reading", + "difficulty": 4, + "tags": [ + "opentelemetry", + "kubernetes", + "prometheus", + "loki" + ], + "question": "以下是一个 OpenTelemetry Collector 的部署配置,阅读后回答问题。", + "code": "apiVersion: v1\nkind: ConfigMap\nmetadata:\n name: otel-collector-config\n namespace: observability\ndata:\n config.yaml: |\n receivers:\n otlp:\n protocols:\n grpc:\n endpoint: 0.0.0.0:4317\n http:\n endpoint: 0.0.0.0:4318\n prometheus:\n config:\n scrape_configs:\n - job_name: 'kubernetes-pods'\n kubernetes_sd_configs:\n - role: pod\n relabel_configs:\n - source_labels: [__meta_kubernetes_pod_annotation_prometheus_io_scrape]\n action: keep\n regex: true\n processors:\n batch:\n timeout: 5s\n send_batch_size: 1000\n memory_limiter:\n check_interval: 1s\n limit_mib: 512\n spike_limit_mib: 128\n attributes:\n actions:\n - key: environment\n action: upsert\n value: production\n exporters:\n otlp/traces:\n endpoint: jaeger-collector.observability:4317\n tls:\n insecure: false\n prometheus:\n endpoint: 0.0.0.0:8889\n namespace: otel\n loki:\n endpoint: http://loki-gateway.observability:3100/loki/api/v1/push\n service:\n pipelines:\n traces:\n receivers: [otlp]\n processors: [memory_limiter, batch]\n exporters: [otlp/traces]\n metrics:\n receivers: [otlp, prometheus]\n processors: [attributes, batch]\n exporters: [prometheus]\n logs:\n receivers: [otlp]\n processors: [memory_limiter, batch]\n exporters: [loki]", + "language": "yaml", + "explanation": "本题考察 OpenTelemetry Collector 的完整配置,包括接收器、处理器、导出器和管道的组织方式。需要理解 OTel Collector 的数据流架构和各组件的作用。", + "source": null, + "related": [], + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "在 traces pipeline 中,processors 的配置顺序是 [memory_limiter, batch]。memory_limiter 放在 batch 之前的原因是什么?", + "options": { + "A": "为了先对数据进行压缩再批处理", + "B": "为了在批处理之前检查内存使用量,在内存超限时拒绝接收新数据,防止 OOM 崩溃", + "C": "memory_limiter 必须始终在 batch 之前,这是 OTel Collector 的硬性要求", + "D": "为了先过滤不需要的 trace 再发送" + }, + "answer": "B", + "explanation": "memory_limiter 是 OTel Collector 的保护性处理器,它在每 1 秒(check_interval)检查进程内存使用量。当内存超过 limit_mib(512MB)时,它会触发垃圾回收;当内存超过 limit_mib + spike_limit_mib(640MB)时,会拒绝接收新数据并返回 429 错误。将其放在 batch 之前,是因为 batch 处理会批量积累数据,如果不先进行内存检查,可能导致内存持续增长直至 OOM。这是 OTel Collector 的最佳实践配置顺序:memory_limiter → batch。" + }, + { + "index": 2, + "type": "short_answer", + "question": "该 OTel Collector 配置了三个 pipeline(traces、metrics、logs),请简述每条 pipeline 的数据流向,包括接收器、处理器和导出器分别是什么?", + "answer": "traces pipeline: OTLP → memory_limiter → batch → Jaeger (OTLP/gRPC)。metrics pipeline: OTLP + Prometheus (auto-discovery) → attributes (注入 environment 标签) → batch → Prometheus exporter (暴露 :8889 供 Prometheus 抓取)。logs pipeline: OTLP → memory_limiter → batch → Loki (HTTP push)。", + "keywords": [ + "traces", + "metrics", + "logs", + "OTLP", + "Prometheus", + "Loki", + "Jaeger" + ], + "scoring_rubric": "traces 流向正确(1分)、metrics 流向正确含 attributes 处理(1分)、logs 流向正确(1分),共3分。", + "explanation": "OTel Collector 的核心架构是 Receiver → Processor → Exporter 的管道模式。traces pipeline 通过 OTLP 接收链路追踪数据,经过内存保护和批处理后导出到 Jaeger。metrics pipeline 同时从 OTLP 和 Prometheus 自动发现接收指标数据,通过 attributes 处理器为所有指标注入 environment=production 标签,再通过 Prometheus exporter 暴露在 :8889 端口供 Prometheus 抓取。logs pipeline 通过 OTLP 接收日志数据,经保护和批处理后推送到 Loki。三条 pipeline 共享 receivers(如 OTLP),但各自的 processor 和 exporter 配置独立。" + }, + { + "index": 3, + "type": "single_choice", + "question": "prometheus receiver 配置中的 kubernetes_sd_configs role=pod 和 relabel_configs 用于什么目的?", + "options": { + "A": "从 Kubernetes API 发现 Pod 并根据 prometheus.io/scrape 注解决定是否抓取该 Pod 的 metrics", + "B": "将 Pod 标签同步到 Prometheus 的服务发现配置", + "C": "自动为所有 Pod 安装 Prometheus exporter", + "D": "从 Kubernetes API 发现 Service 并抓取 Service 的 metrics" + }, + "answer": "A", + "explanation": "kubernetes_sd_configs 配置 role=pod 表示 OTel Collector 会通过 Kubernetes API 自动发现集群中的所有 Pod。relabel_configs 中的 action=keep 配合 regex: true 表示只有当 Pod 带有 prometheus.io/scrape=true 注解时,才会被纳入抓取目标。这是云原生环境下 Prometheus 动态服务发现的标准模式——应用只需添加注解即可自动被监控,无需修改 Prometheus 配置。这种零侵入式的监控集成方式在微服务架构中尤为重要。" + } + ] + } + ] +} \ No newline at end of file diff --git a/topics/interview-prep/k8s-observability/fill_blank.json b/topics/interview-prep/k8s-observability/fill_blank.json new file mode 100644 index 0000000..11fbeb8 --- /dev/null +++ b/topics/interview-prep/k8s-observability/fill_blank.json @@ -0,0 +1,193 @@ +{ + "topic": "k8s-observability", + "type": "fill_blank", + "schema_version": "1.0.0", + "generated": "2026-09-09T16:10:00+08:00", + "questions": [ + { + "id": "fb-001", + "type": "fill_blank", + "difficulty": 2, + "tags": [ + "kubernetes", + "pod", + "deployment" + ], + "question": "在 Kubernetes 中,Deployment 通过 ______ 控制器来管理 Pod 的副本数量,确保指定数量的 Pod 始终处于运行状态。", + "answer": [ + "ReplicaSet" + ], + "answer_rule": "any", + "explanation": "Deployment 的底层是 ReplicaSet 控制器。Deployment 对象声明期望的 Pod 模板和副本数,ReplicaSet 负责实际维护 Pod 的生命周期,确保运行中的 Pod 数量与 spec.replicas 一致。当 Pod 意外终止时,ReplicaSet 会自动创建新 Pod 进行替换。", + "source": null, + "related": [] + }, + { + "id": "fb-002", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "kubernetes", + "ingress", + "service" + ], + "question": "Kubernetes 中,______ 是一种 API 对象,用于管理集群中 Service 的外部 HTTP/HTTPS 访问,提供基于名称的虚拟主机和基于路径的路由规则。", + "answer": [ + "Ingress" + ], + "answer_rule": "any", + "explanation": "Ingress 资源定义了从集群外部到内部 Service 的 HTTP/HTTPS 路由规则。它通常配合 Ingress Controller(如 Nginx Ingress Controller)工作,支持基于主机名和 URL 路径的流量转发。Ingress 可以配置 TLS 终止、重写规则等,是 Kubernetes 中暴露服务的标准方式之一。", + "source": null, + "related": [] + }, + { + "id": "fb-003", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "kubernetes", + "rolling-update", + "deployment" + ], + "question": "在 Kubernetes Rolling Update 策略中,maxSurge 控制更新过程中可以超出期望副本数的最大 Pod 数量,而 ______ 控制更新过程中可以处于不可用状态的最大 Pod 数量。", + "answer": [ + "maxUnavailable" + ], + "answer_rule": "any", + "explanation": "Rolling Update 策略通过两个关键参数控制发布节奏:maxSurge 决定可以额外创建多少个新 Pod(默认 25%),maxUnavailable 决定可以有多少个旧 Pod 被标记为不可用(默认 25%)。两者配合确保在更新过程中始终有足够数量的可用 Pod 服务流量,同时逐步用新版本替换旧版本。", + "source": null, + "related": [] + }, + { + "id": "fb-004", + "type": "fill_blank", + "difficulty": 2, + "tags": [ + "kubernetes", + "configmap", + "secret" + ], + "question": "在 Kubernetes 中,______ 资源用于存储敏感信息(如密码、令牌、证书),数据以 Base64 编码存储,但不提供加密功能,需配合 RBAC 进行访问控制。", + "answer": [ + "Secret" + ], + "answer_rule": "any", + "explanation": "Secret 是 Kubernetes 中专门用于管理敏感数据的资源对象。它以 Base64 编码存储数据(注意 Base64 是编码而非加密)。生产环境中建议结合 etcd 加密(EncryptionConfiguration)来保护 Secret 数据。Secret 可以通过环境变量或 Volume 挂载方式注入到 Pod 中。", + "source": null, + "related": [] + }, + { + "id": "fb-005", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "canary", + "blue-green" + ], + "question": "在灰度发布策略中,Canary(金丝雀)发布是将新版本逐步推送给 ______ 的用户,观察其行为和指标,确认无误后再全量发布;而 Blue-Green(蓝绿)发布则维护两套完全相同的生产环境。", + "answer": [ + "小部分", + "少量", + "一部分", + "部分" + ], + "answer_rule": "any", + "explanation": "Canary 发布的核心思想是风险控制:先将新版本部署给一小部分用户(如 5% 的流量),通过监控指标(错误率、延迟等)验证新版本的稳定性,然后逐步增加流量比例直到全量切换。相比 Blue-Green 的一次性切换,Canary 发布更加渐进,回滚成本更低,适合对稳定性要求极高的场景。", + "source": null, + "related": [] + }, + { + "id": "fb-006", + "type": "fill_blank", + "difficulty": 4, + "tags": [ + "HPA", + "VPA", + "KEDA" + ], + "question": "Kubernetes 的 HPA(Horizontal Pod Autoscaler)基于 CPU、内存等指标自动扩缩 Pod 的副本数量;______(Vertical Pod Autoscaler)则根据资源使用情况自动调整 Pod 的 CPU 和内存请求与限制。", + "answer": [ + "VPA", + "Vertical Pod Autoscaler" + ], + "answer_rule": "any", + "explanation": "VPA 通过分析 Pod 的历史资源使用数据,自动调整容器的 resource requests 和 limits,实现「垂直」维度的资源优化。VPA 有三种模式:Off(仅建议)、Initial(仅在 Pod 创建时应用)、Auto(自动驱逐并重建 Pod 以应用新资源)。与 HPA 的水平扩缩不同,VPA 更适合无法水平扩展的应用(如单体数据库)。KEDA 则是基于事件驱动的高级自动伸缩器,支持更丰富的触发器。", + "source": null, + "related": [] + }, + { + "id": "fb-007", + "type": "fill_blank", + "difficulty": 2, + "tags": [ + "kubernetes", + "liveness", + "readiness" + ], + "question": "Kubernetes 中的存活探针(______ Probe)用于检测容器是否仍在运行,如果探针失败,kubelet 会重启该容器。", + "answer": [ + "liveness", + "Liveness" + ], + "answer_rule": "any", + "explanation": "Liveness Probe 用于检测容器是否存活。如果 Liveness Probe 失败,kubelet 会认为容器处于不健康状态并执行重启操作。常见的探针方式包括 HTTP GET(检查端口是否返回 200)、TCP Socket(检查端口是否可达)、Exec(执行命令检查退出码)。Liveness Probe 适用于检测死锁、无限循环等无法自行恢复的故障场景。", + "source": null, + "related": [] + }, + { + "id": "fb-008", + "type": "fill_blank", + "difficulty": 2, + "tags": [ + "prometheus", + "grafana" + ], + "question": "Prometheus 使用 ______ 查询语言(PromQL)来检索和聚合时间序列数据,例如通过 rate() 计算指标的变化速率。", + "answer": [ + "PromQL", + "Prometheus Query Language" + ], + "answer_rule": "any", + "explanation": "PromQL 是 Prometheus 内置的函数式查询语言,支持选择器、聚合操作(sum、avg、max 等)、数学运算和内置函数(rate、histogram_quantile 等)。例如 rate(http_requests_total[5m]) 表示计算过去 5 分钟内 HTTP 请求的每秒增长率。PromQL 是使用 Prometheus 进行监控告警和数据可视化的基础。", + "source": null, + "related": [] + }, + { + "id": "fb-009", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "opentelemetry", + "OTLP" + ], + "question": "OpenTelemetry 定义了 ______(OpenTelemetry Protocol)作为其默认的数据传输协议,用于统一传输 Traces、Metrics 和 Logs 三种遥测数据。", + "answer": [ + "OTLP", + "OpenTelemetry Protocol" + ], + "answer_rule": "any", + "explanation": "OTLP 是 OpenTelemetry 原生的传输协议,支持 gRPC 和 HTTP/protobuf 两种传输方式。OTLP 的设计目标是统一 Traces(分布式追踪)、Metrics(指标)和 Logs(日志)的采集与传输格式,使应用只需集成一个 SDK 就能输出所有类型的遥测数据,再由 OTel Collector 进行统一处理、转换和导出到后端存储。", + "source": null, + "related": [] + }, + { + "id": "fb-010", + "type": "fill_blank", + "difficulty": 4, + "tags": [ + "loki", + "opentelemetry", + "grafana" + ], + "question": "在可观测性技术栈中,______ 是 Grafana 推出的日志聚合系统,它不建立全文索引,而是只索引日志的标签(labels),因此查询时需要先通过标签过滤再进行正则匹配。", + "answer": [ + "Loki", + "Grafana Loki" + ], + "answer_rule": "any", + "explanation": "Loki 的设计理念是「像 Prometheus 一样索引标签」——它只为日志的元数据标签(如 app、env、namespace)建立索引,而不对日志内容建立全文索引。这种设计使得 Loki 的存储成本远低于 Elasticsearch(ELK 栈),但查询日志内容时需要先用标签缩小范围再进行 LogQL 正则匹配。Loki 与 Grafana 深度集成,可以在 Grafana 面板中直接查询和关联日志数据。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/interview-prep/k8s-observability/meta.json b/topics/interview-prep/k8s-observability/meta.json new file mode 100644 index 0000000..4bc71b2 --- /dev/null +++ b/topics/interview-prep/k8s-observability/meta.json @@ -0,0 +1,25 @@ +{ + "slug": "k8s-observability", + "name": "K8s与可观测性", + "description": "K8s应用发布、弹性扩缩容、灰度发布、健康检查、可观测体系建设", + "tags": [], + "question_files": { + "single_choice": "single_choice.json", + "true_false": "true_false.json", + "fill_blank": "fill_blank.json", + "short_answer": "short_answer.json", + "code_reading": "code_reading.json" + }, + "stats": { + "total": 45, + "by_type": { + "single_choice": 15, + "true_false": 10, + "fill_blank": 10, + "short_answer": 5, + "code_reading": 5 + } + }, + "created": "2026-09-09", + "updated": "2026-09-09" +} \ No newline at end of file diff --git a/topics/interview-prep/k8s-observability/short_answer.json b/topics/interview-prep/k8s-observability/short_answer.json new file mode 100644 index 0000000..28e0141 --- /dev/null +++ b/topics/interview-prep/k8s-observability/short_answer.json @@ -0,0 +1,164 @@ +{ + "topic": "k8s-observability", + "type": "short_answer", + "schema_version": "1.0.0", + "generated": "2026-09-09T16:10:00+08:00", + "questions": [ + { + "id": "sa-001", + "type": "short_answer", + "difficulty": 3, + "tags": [ + "kubernetes", + "rolling-update", + "blue-green", + "canary", + "deployment" + ], + "question": "请对比 Kubernetes 中 Rolling Update、Blue-Green 和 Canary 三种应用发布策略的核心机制、适用场景和主要优缺点。", + "answer": "Rolling Update(滚动更新):逐步用新版 Pod 替换旧版 Pod,通过 maxSurge 和 maxUnavailable 控制更新节奏。优点是无需额外资源、K8s 原生支持;缺点是回滚速度较慢,且在更新期间新旧版本共存可能产生兼容性问题。适用于大多数常规服务更新。\n\nBlue-Green(蓝绿部署):同时维护两套完整环境(Blue 为当前版本,Green 为新版本),通过切换 Service selector 或 Ingress 流量一次性将所有流量切到新环境。优点是回滚极快(切回 Blue 即可)、部署原子性强;缺点是需要双倍资源,且切换瞬间可能产生短暂不一致。适用于对可用性要求极高且资源充足的核心服务。\n\nCanary(金丝雀发布):先将少量流量(如 5%)导到新版本,观察监控指标无异常后逐步扩大比例,最终全量切换。优点是风险可控、可提前发现生产问题;缺点是实现复杂度高(需要 Service Mesh 或 Ingress 精细流量控制)、新旧版本长期共存需保证兼容。适用于大型服务或变更影响面广的场景。", + "keywords": [ + "滚动更新", + "maxSurge", + "maxUnavailable", + "蓝绿部署", + "双倍资源", + "金丝雀", + "流量比例", + "回滚", + "Service Mesh" + ], + "scoring_rubric": "需完整对比三种策略的核心机制(各1分),正确说明适用场景(各0.5分),准确指出优缺点(各0.5分)。只提两种策略扣2分,缺少具体技术细节(如 maxSurge、Service selector 切换、流量比例控制)扣1分。", + "explanation": "K8s 原生 Deployment 默认支持 Rolling Update,可通过 spec.strategy 配置 maxSurge(最大超出期望副本数)和 maxUnavailable(最大不可用副本数)来控制滚动节奏。Blue-Green 部署通常需要维护两个 Deployment 并通过切换 Service 的 selector 或使用 Argo Rollouts 等工具实现流量切换。Canary 发布在原生 K8s 中可通过 Deployment 副本数比例控制,更精细的方案需要借助 Istio 等 Service Mesh 或 Flagger 等 GitOps 工具。三种策略的选择取决于团队对风险的容忍度、资源预算和运维复杂度的权衡。", + "source": null, + "related": [] + }, + { + "id": "sa-002", + "type": "short_answer", + "difficulty": 3, + "tags": [ + "kubernetes", + "HPA", + "VPA", + "pod", + "deployment" + ], + "question": "请说明 Kubernetes 中 HPA(Horizontal Pod Autoscaler)的工作原理,并对比 HPA 与 VPA(Vertical Pod Autoscaler)的适用场景和限制。", + "answer": "HPA 工作原理:HPA 通过周期性(默认 15 秒)查询 Metrics Server 或自定义 metrics API 获取指标(如 CPU 利用率、内存使用量或自定义指标),将当前指标值与用户设定的目标值进行比较,计算出期望副本数(desiredReplicas = ceil(currentReplicas × (currentMetricValue / desiredMetricValue)))。然后通过调整 Deployment/ReplicaSet 的 replicas 字段实现水平扩缩容。HPA 支持 stabilizationWindowSeconds 来防止抖动,也支持多指标和行为配置(Behavior)来精细控制扩缩策略。\n\nHPA vs VPA 对比:HPA 通过增减 Pod 副本数实现扩容,适用于无状态、可水平扩展的应用(如 Web 服务、API 网关)。VPA 通过调整单个 Pod 的 CPU request/limits 和内存 request/limits 实现垂直扩容,适用于有状态应用或无法水平扩展的场景(如单体数据库)。HPA 和 VPA 默认不能同时作用于同一资源(VPA 会驱逐 Pod 来应用新资源规格,与 HPA 冲突),但 VPA 可以仅设置 recommend 模式配合 HPA 使用。VPA 的缩容需要 Pod 重启,有一定影响;HPA 则可以更平滑地增减副本。", + "keywords": [ + "HPA", + "Metrics Server", + "副本数", + "目标值", + "desiredReplicas", + "VPA", + "resource request", + "水平扩展", + "垂直扩展", + "stabilizationWindow", + "不能同时使用" + ], + "scoring_rubric": "HPA 工作原理描述正确(含指标获取、计算公式、执行动作)得3分。HPA 与 VPA 对比正确(适用场景各1分,限制/冲突1分)得3分。缺少具体计算公式扣1分,未提及 HPA 与 VPA 不能同时使用扣1分。", + "explanation": "HPA 的核心是反馈控制循环(reconciliation loop):采集指标 → 计算期望副本数 → 调整 replicas。计算公式为 desiredReplicas = ceil(currentReplicas × (currentMetric / desiredMetric))。例如当前 CPU 利用率 80%,目标 40%,则副本数翻倍。HPA 的 Behavior 配置允许分别设置扩容和缩容的策略(如 scaleUp 更激进、scaleDown 更保守)。VPA 通过 Admission Controller 在 Pod 创建时注入资源建议,或通过 Updater 驱逐 Pod 以应用新资源规格,这决定了它不能与 HPA 同时调节同一维度。在生产实践中,推荐用 HPA 处理流量波动,用 VPA 的 Auto Off 或 Recommendation 模式辅助设定合理的 resource request。", + "source": null, + "related": [] + }, + { + "id": "sa-003", + "type": "short_answer", + "difficulty": 2, + "tags": [ + "kubernetes", + "liveness", + "readiness", + "pod", + "startup" + ], + "question": "请说明 Kubernetes 中 Liveness Probe、Readiness Probe 和 Startup Probe 三种健康检查探针的区别、各自作用和典型使用场景。", + "answer": "Liveness Probe(存活探针):检测容器是否仍在正常运行。如果 Liveness Probe 失败,kubelet 会杀死容器并根据 restartPolicy 重启。适用于检测死锁、内存泄漏等导致进程假死但未退出的场景。例如:一个 Web 服务进程仍在监听端口但无法处理请求(死锁),Liveness Probe 检测到后触发重启。\n\nReadiness Probe(就绪探针):检测容器是否准备好接收流量。如果 Readiness Probe 失败,K8s 会从 Service 的 Endpoints 中移除该 Pod,使其不再接收新请求,但不会重启容器。适用于容器需要较长启动时间加载数据、或临时不可用(如正在做数据同步)的场景。例如:数据库从库正在同步数据,Readiness Probe 返回失败,流量不会路由到该 Pod。\n\nStartup Probe(启动探针):检测容器中的应用是否已成功启动。在 Startup Probe 成功之前,其他探针(Liveness/Readiness)都不会执行。适用于启动时间很长的应用(如大型 Java 应用加载 Spring 上下文),避免 Liveness Probe 在应用启动阶段误判导致反复重启。Startup Probe 成功后,由 Liveness/Readiness Probe 接管后续健康检查。", + "keywords": [ + "Liveness Probe", + "存活探针", + "重启容器", + "Readiness Probe", + "就绪探针", + "Endpoints", + "流量摘除", + "Startup Probe", + "启动探针", + "延迟执行" + ], + "scoring_rubric": "三种探针各2分:名称和作用描述正确1分,失败后果和使用场景正确1分。缺少任一探针扣2分。未说明 Startup Probe 与其他两种探针的执行顺序关系扣1分。", + "explanation": "三种探针的设计体现了关注点分离原则:Liveness 关注「进程是否健康」,Readiness 关注「能否接流量」,Startup 关注「是否启动完成」。在实际配置中,Liveness 的 initialDelaySeconds 应设置得保守一些(避免应用还在初始化就被杀),而 Startup Probe 可以设置较大的 failureThreshold × periodSeconds 来给应用充足的启动时间。一个常见的最佳实践是:对于启动快的服务只配 Liveness + Readiness;对于启动慢的服务(如 Java Spring Boot)三者都配,并将 Startup Probe 的超时时间设为 Liveness Probe 初始延迟的数倍。", + "source": null, + "related": [] + }, + { + "id": "sa-004", + "type": "short_answer", + "difficulty": 4, + "tags": [ + "prometheus", + "grafana", + "loki", + "kubernetes", + "opentelemetry" + ], + "question": "请设计一个基于 Kubernetes 的完整可观测性体系,说明如何覆盖 Metrics、Logs、Traces 三大支柱,并阐述各组件的选型理由和数据流向。", + "answer": "完整的可观测性体系需要覆盖三大支柱:\n\n**Metrics(指标)层:**\n- 采集:Prometheus(通过 ServiceMonitor/PodMonitor CRD 自动发现 K8s 中的服务并抓取指标)+ node-exporter(节点级指标)+ kube-state-metrics(K8s 对象状态指标)。\n- 存储:Prometheus 本地存储或 Thanos/Mimir 实现长期存储和高可用。\n- 展示:Grafana 作为统一可视化面板,通过 Prometheus 数据源查询和展示指标。\n\n**Logs(日志)层:**\n- 采集:Promtail 或 Alloy 以 DaemonSet 方式部署在每个节点,采集容器 stdout/stderr 日志(遵循 K8s 日志约定),也可通过 OpenTelemetry Collector 采集。\n- 存储与查询:Grafana Loki(标签索引 + 原始日志存储,不全文索引,成本低)。\n- 展示:Grafana 通过 Loki 数据源查询日志,支持日志与指标的关联跳转。\n\n**Traces(链路追踪)层:**\n- 采集:应用通过 OpenTelemetry SDK 埋点,导出 OTLP 格式 traces 到 OpenTelemetry Collector。\n- 处理与导出:OTel Collector 负责接收、处理(采样、批处理、属性注入)和导出,后端可选 Jaeger、Tempo 或 Zipkin。\n- 展示:Grafana Tempo(原生支持 TraceQL)或 Jaeger UI。\n\n**数据流向总结:**\n应用 → OTel SDK → OTel Collector → 各后端(Prometheus/Loki/Tempo)→ Grafana 统一展示。Prometheus 同时作为 OTel Collector 的 metrics 导出目标。\n\n**选型理由:** Grafana 全家栈(Prometheus + Loki + Tempo + Grafana)能实现三大支柱的无缝关联:在 Grafana 中可以从一个 metric 异常跳转到关联的 traces,再从 traces 跳转到对应 logs,形成完整的故障排查链路。", + "keywords": [ + "Metrics", + "Logs", + "Traces", + "Prometheus", + "Loki", + "Grafana", + "OpenTelemetry", + "OTel Collector", + "ServiceMonitor", + "Promtail", + "Tempo", + "OTLP", + "三大支柱", + "Grafana 全家栈" + ], + "scoring_rubric": "三大支柱各占2分(覆盖 Metrics/Logs/Traces 各层的组件选型和数据流),总计6分。每缺一个支柱扣2分。组件选型有合理解释得1分,数据流向描述清晰得1分。未提及 OTel Collector 作为统一入口扣1分,未说明 Grafana 统一展示扣1分。总分上限8分。", + "explanation": "可观测性体系的核心挑战在于三大支柱的割裂——指标、日志、链路追踪通常由不同工具管理,排查问题时需要在多个系统间切换。Grafana 全家栈方案的优势在于统一的查询界面和原生的支柱间关联能力。OpenTelemetry 作为 CNCF 标准化项目,提供厂商无关的 SDK 和 Collector,避免了 vendor lock-in。在 K8s 环境中,Prometheus 的服务发现机制(基于 label 和 annotations)使其天然适配动态 Pod 环境。Loki 的设计哲学是「like Prometheus, but for logs」——只索引标签不索引全文,大幅降低存储成本。Tempo 则是分布式追踪后端,支持从日志中自动提取 trace ID 实现 logs-to-traces 关联。", + "source": null, + "related": [] + }, + { + "id": "sa-005", + "type": "short_answer", + "difficulty": 3, + "tags": [ + "opentelemetry", + "kubernetes", + "pod", + "prometheus" + ], + "question": "请说明 OpenTelemetry 的架构组成、各组件职责,以及在 Kubernetes 环境中的典型数据流。", + "answer": "OpenTelemetry(OTel)的架构由三个核心部分组成:\n\n**1. API & SDK:**\n- API:定义了 Traces、Metrics、Logs 的标准接口(如 Tracer、Meter、Logger),是应用埋点的抽象层。\n- SDK:API 的具体实现,负责数据的采集、处理(采样、批处理、过滤)和导出。应用通过 SDK 创建 spans、记录 metrics 和 logs。\n\n**2. OpenTelemetry Collector:**\n- 核心组件,部署为独立进程或 sidecar/daemonset,负责接收、处理和导出遥测数据。\n- 架构分为三个管道:Receiver(接收数据,如 OTLP receiver 接收 OTLP 格式)→ Processor(处理数据,如 batch、memory limiter、tail sampling、attributes 处理)→ Exporter(导出到后端,如 Prometheus exporter、Loki exporter、OTLP exporter 到 Jaeger/Tempo)。\n- 支持多种部署模式:sidecar(每个 Pod 一个 Collector)、DaemonSet(每个节点一个)、Deployment(集中式 Gateway)。\n\n**3. 协议与传播:**\n- OTLP(OpenTelemetry Protocol):默认的传输协议,支持 gRPC 和 HTTP,用于 SDK → Collector 和 Collector → Backend 的数据传输。\n- W3C TraceContext:分布式追踪的上下文传播标准,通过 HTTP Header 传递 trace-id 和 span-id,实现跨服务的链路关联。\n\n**K8s 环境中的典型数据流:**\n应用 Pod 内嵌 OTel SDK(自动/手动埋点)→ 通过 OTLP 导出到同节点的 Collector(DaemonSet 模式)或 Pod sidecar → Collector 做批处理和采样 → 通过 OTLP Exporter 发送到集中式 Collector Gateway → Gateway 分发到各后端(Prometheus、Tempo、Loki)。对于 K8s 基础设施指标,OTel Collector 还可以通过 k8s_cluster receiver 自动采集集群级指标。", + "keywords": [ + "OpenTelemetry", + "API", + "SDK", + "OTel Collector", + "Receiver", + "Processor", + "Exporter", + "OTLP", + "W3C TraceContext", + "sidecar", + "DaemonSet", + "采样", + "上下文传播" + ], + "scoring_rubric": "OTel 三大组成部分(API/SDK、Collector、协议)各1分,共3分。Collector 内部三管道(Receiver→Processor→Exporter)正确描述得1分。K8s 数据流描述正确(含部署模式)得2分。未提及 OTLP 协议扣1分,未提及 W3C TraceContext 扣1分。总分上限6分。", + "explanation": "OpenTelemetry 是 CNCF 中仅次于 Kubernetes 的第二活跃项目,其设计目标是提供统一的可观测性数据采集标准,解决 vendor lock-in 问题。API 和 SDK 的分离使得应用代码只依赖轻量级 API,实际采集逻辑由 SDK 提供,方便替换。Collector 的管道架构(Receiver→Processor→Exporter)是其核心设计,通过 YAML 配置即可灵活组合。在 K8s 中推荐使用 OTel Operator 自动注入 SDK(通过 instrumentation annotation)和管理 Collector 实例。W3C TraceContext 传播机制确保了在微服务调用链中 trace-id 的一致性传递,这是实现分布式追踪的基础。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/interview-prep/k8s-observability/single_choice.json b/topics/interview-prep/k8s-observability/single_choice.json new file mode 100644 index 0000000..1559349 --- /dev/null +++ b/topics/interview-prep/k8s-observability/single_choice.json @@ -0,0 +1,326 @@ +{ + "topic": "k8s-observability", + "type": "single_choice", + "schema_version": "1.0.0", + "generated": "2026-09-09T16:10:00+08:00", + "questions": [ + { + "id": "sc-001", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "kubernetes", + "deployment", + "rolling-update" + ], + "question": "在 Kubernetes 中,Deployment 的 spec.strategy.rollingUpdate 配置 maxSurge: 25% 和 maxUnavailable: 25%,当集群中有 10 个 Pod 且需要更新到新版本时,最多会同时运行多少个新版本的 Pod?", + "options": { + "A": "2 个", + "B": "3 个", + "C": "5 个", + "D": "10 个" + }, + "answer": "C", + "explanation": "maxSurge: 25% 表示最多可以超出期望副本数 25%,即 10 × 25% = 2.5,向上取整为 3,所以最多总 Pod 数为 10 + 3 = 13。maxUnavailable: 25% 表示最多有 25% 的 Pod 不可用,即 10 × 25% = 2.5,向上取整为 3,即最少需要 7 个可用 Pod。在滚动更新过程中,系统先创建新 Pod,新 Pod 就绪后才能终止旧 Pod。新 Pod 逐批创建时,当 3 个新 Pod 全部就绪且旧 Pod 还未完全终止,此时最多可同时运行 5 个新版本 Pod(13 个总 Pod 减去 8 个旧版本 = 5 个新版本)。但更直观的理解:每批次 maxSurge 允许额外创建 3 个新 Pod,加上同时允许不可用的 3 个旧 Pod,新版本最多可以有 5 个同时运行(7 个旧 + 5 个新 = 12 ≤ 13)。", + "source": null, + "related": [] + }, + { + "id": "sc-002", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "kubernetes", + "pod", + "liveness", + "readiness" + ], + "question": "关于 Kubernetes 中三种探针的作用,以下说法正确的是?", + "options": { + "A": "Liveness Probe 失败后,Pod 会被重新调度到其他节点", + "B": "Readiness Probe 失败后,Pod 会被从 Service 的 Endpoints 中移除", + "C": "Startup Probe 在容器启动成功后会持续探测", + "D": "三种探针的检测结果都直接影响 Pod 的驱逐决策" + }, + "answer": "B", + "explanation": "Readiness Probe 失败后,Pod 会被标记为 NotReady,并从关联 Service 的 Endpoints 列表中移除,从而不再接收流量。A 错误:Liveness Probe 失败后,kubelet 会重启该容器(restartPolicy 决定是否重建 Pod),而非调度到其他节点。C 错误:Startup Probe 只在容器启动阶段运行,一旦成功,后续不再探测,而是由 Liveness 和 Readiness 接管。D 错误:只有 Liveness Probe 失败会触发容器重启,Readiness 和 Startup 的结果不会导致 Pod 被驱逐。", + "source": null, + "related": [] + }, + { + "id": "sc-003", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "kubernetes", + "service", + "ingress", + "declarative-api" + ], + "question": "以下关于 Kubernetes Service 类型与 Ingress 的描述,错误的是?", + "options": { + "A": "ClusterIP 类型的 Service 只能在集群内部访问", + "B": "NodePort 会在每个节点上开放一个固定端口,外部流量通过 : 访问", + "C": "Ingress 资源可以提供 L7 层的 HTTP 路由和 TLS 终止,但需要 Ingress Controller 才能生效", + "D": "LoadBalancer 类型的 Service 在任何云环境中都会自动创建外部负载均衡器,无需云厂商支持" + }, + "answer": "D", + "explanation": "LoadBalancer 类型的 Service 需要云厂商的 LoadBalancer 实现支持(如 AWS ELB、GCP Cloud Load Balancer)。在本地环境(如 Minikube、Kind)或没有云负载均衡器的裸金属环境中,LoadBalancer 类型可能无法正常工作或需要 MetalLB 等额外组件。A 正确:ClusterIP 是默认类型,仅集群内可达。B 正确:NodePort 在 30000-32767 范围内开放端口。C 正确:Ingress 本身是声明式的路由规则定义,需要 Ingress Controller(如 nginx-ingress、traefik)来实际执行路由。", + "source": null, + "related": [] + }, + { + "id": "sc-004", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "kubernetes", + "canary", + "blue-green", + "flagger" + ], + "question": "在使用 Flagger 进行 Canary 发布时,以下哪个指标必须配置且 Flagger 默认会等待其达标后才继续推进金丝雀?", + "options": { + "A": "请求成功率(Request Success Rate)", + "B": "请求延迟 P99(Request Duration)", + "C": "HTTP 5xx 错误率(Request Error Rate)", + "D": "以上三项都是 Flagger 的默认分析指标" + }, + "answer": "D", + "explanation": "Flagger 默认的 Canary 分析会检查三项核心指标:请求成功率(默认阈值 99%)、请求延迟(默认 P99 ≤ 500ms)、HTTP 5xx 错误率(默认阈值 1%)。这三项指标来自 Flagger 集成的 Prometheus 查询,如果任一指标不达标,Flagger 会自动回滚金丝雀。虽然理论上可以通过 CanaryAnalysis 配置只选部分指标,但 Flagger 的默认模板会同时包含这三项,这也是业界金丝雀发布的最佳实践。", + "source": null, + "related": [] + }, + { + "id": "sc-005", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "kubernetes", + "hpa", + "custom-metrics" + ], + "question": "关于 Kubernetes HPA(Horizontal Pod Autoscaler)V2 版本的配置,以下哪个说法是正确的?", + "options": { + "A": "HPA V2 只能基于 CPU 和 Memory 进行自动扩缩容", + "B": "HPA V2 支持基于 Custom Metrics 和 External Metrics 进行扩缩容", + "C": "HPA V2 的 behavior 字段只能配置 scaleUp 操作,不能配置 scaleDown", + "D": "HPA V2 检测到指标变化后会在 15 秒内立即调整副本数,没有任何延迟" + }, + "answer": "B", + "explanation": "HPA V2(autoscaling/v2)是 HPA 的增强版本,除了支持 CPU 和 Memory 这两个内置指标外,还支持 Custom Metrics(集群内资源暴露的指标,如每秒请求数)和 External Metrics(集群外部指标,如消息队列的队列长度)。A 错误:V2 的核心优势就是支持自定义和外部指标。C 错误:behavior 字段同时支持 scaleUp 和 scaleDown 两个方向的配置,可以分别设定稳定窗口、步进策略等。D 错误:HPA 有 stabilizationWindowSeconds 默认值(scaleUp 为 0,scaleDown 为 300),用于避免频繁抖动。", + "source": null, + "related": [] + }, + { + "id": "sc-006", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "prometheus", + "grafana", + "servicemonitor" + ], + "question": "在 Prometheus Operator 中,ServiceMonitor 资源的正确作用是?", + "options": { + "A": "直接向 Prometheus 发送告警", + "B": "定义 Prometheus 应该从哪些 Service 抓取(scrape)指标", + "C": "存储 Prometheus 的长期历史数据", + "D": "定义 Grafana Dashboard 的可视化面板" + }, + "answer": "B", + "explanation": "ServiceMonitor 是 Prometheus Operator 提供的 CRD(Custom Resource Definition),用于声明式地定义 Prometheus 的抓取目标。它通过 label selector 匹配 Service,告诉 Prometheus Controller 应该监控哪些 Service 的哪个端口和路径。A 错误:告警由 PrometheusRule 资源定义。C 错误:长期存储由 Thanos 或 VictoriaMetrics 等组件处理,或通过 Prometheus 的 remote write 实现。D 错误:Grafana Dashboard 通过 JSON 定义或 Grafana UI 创建,与 ServiceMonitor 无关。", + "source": null, + "related": [] + }, + { + "id": "sc-007", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "opentelemetry", + "otlp", + "context-propagation" + ], + "question": "关于 OpenTelemetry 的 Context Propagation 机制,以下说法正确的是?", + "options": { + "A": "OpenTelemetry 只支持 W3C TraceContext 一种传播格式,没有其他选择", + "B": "Context Propagation 通过在 HTTP Header 中注入和提取 TraceID、SpanID 等信息,实现跨服务链路追踪", + "C": "Context Propagation 只能用于分布式追踪(Tracing),不能用于传播 Metrics 和 Logs 的上下文", + "D": "W3C TraceContext 标准中 traceparent 头的格式为 trace-id-span-id-trace-flags,不包含 version 和 trace-state" + }, + "answer": "B", + "explanation": "Context Propagation 是 OpenTelemetry 实现跨服务可观测性的核心机制。它通过在服务间调用的 HTTP Header(如 W3C 标准的 traceparent、tracestate)或消息队列的元数据中注入 TraceID、SpanID、TraceFlags 等上下文信息,使下游服务能够提取这些信息并继续追踪链路。A 错误:OpenTelemetry 支持多种传播格式,包括 W3C TraceContext、B3(Zipkin)、Jaeger 等。C 错误:Context Propagation 可以同时传播 Trace、Metric、Log 的关联上下文(Baggage)。D 错误:traceparent 的标准格式为 version-trace-id-parent-id-trace-flags(5 段),且 tracestate 是可选的独立头。", + "source": null, + "related": [] + }, + { + "id": "sc-008", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "loki", + "promtail", + "logging" + ], + "question": "Grafana Loki 与 ELK(Elasticsearch + Logstash + Kibana)在日志架构上的核心区别是?", + "options": { + "A": "Loki 不支持结构化日志,只能处理非结构化文本", + "B": "Loki 不对日志内容建立全文索引,只索引标签(Labels),因此存储成本更低、写入性能更好", + "C": "Loki 只能接收 Kubernetes 环境的日志,不支持裸金属或虚拟机", + "D": "Loki 不支持 PromQL 查询,使用完全不同的查询语法" + }, + "answer": "B", + "explanation": "Loki 的核心设计理念是\"像 Prometheus 一样,但用于日志\"。它不像 Elasticsearch 那样对日志全文建立倒排索引,而是只对元数据标签(Labels)建立索引。这使得 Loki 的写入性能更高、存储成本更低(通常比 Elasticsearch 低数倍)。查询时先通过 Labels 缩小范围,再使用 LogQL 在匹配的日志流中做文本搜索。A 错误:Loki 完全支持结构化日志(JSON 等格式)。C 错误:Loki 通过 Promtail、Fluentd 等 Agent 支持各种环境的日志采集。D 错误:Loki 使用 LogQL 查询语言,语法上与 PromQL 有相似之处。", + "source": null, + "related": [] + }, + { + "id": "sc-009", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "kubernetes", + "configmap", + "secret" + ], + "question": "以下关于 Kubernetes ConfigMap 和 Secret 的描述,正确的是?", + "options": { + "A": "ConfigMap 和 Secret 的内容都会以 base64 编码存储在 etcd 中", + "B": "Secret 的内容是加密存储的,可以直接作为安全凭证使用", + "C": "ConfigMap 可以被挂载为 Volume 或作为环境变量注入 Pod,Secret 也可以", + "D": "ConfigMap 和 Secret 都必须在创建 Pod 之前创建,不支持动态引用" + }, + "answer": "C", + "explanation": "ConfigMap 和 Secret 都支持两种使用方式:一是通过 env 字段注入为 Pod 的环境变量;二是通过 volumes/volumeMounts 挂载为文件。A 错误:ConfigMap 以明文存储(base64 只是 YAML 编码方式,不是加密),Secret 虽然是 base64 编码但这只是编码不是加密(需要配合 encryption at rest 才算真正加密)。B 错误:Secret 的 base64 编码不是加密,任何能访问 etcd 的人都能解码。D 错误:两者都支持在 Pod 创建前先创建,但也支持通过 immutable 配置、外部配置工具(如 External Secrets Operator)动态管理。", + "source": null, + "related": [] + }, + { + "id": "sc-010", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "hpa", + "keda", + "elasticity" + ], + "question": "KEDA(Kubernetes Event-driven Autoscaling)相比原生 HPA 的核心优势是?", + "options": { + "A": "KEDA 支持更丰富的事件源,可以根据 Kafka 消费者组的 lag、RabbitMQ 队列深度、Cron 调度等 60+ 种 Event Source 自动扩缩容", + "B": "KEDA 使用完全不同的 Pod 调度算法,能将 Pod 调度到更合适的节点上", + "C": "KEDA 替代了 Kubernetes 的 Pod 控制器,直接管理 Pod 的生命周期", + "D": "KEDA 只能将副本数扩展到 HPA 上限,无法超越 HPA 的能力" + }, + "answer": "A", + "explanation": "KEDA 是一个 CNCF 毕业项目,它作为 HPA 的增强层工作。KEDA 的核心价值是提供了丰富的 Scaler(伸缩器),支持 60+ 种事件源的自动扩缩容,包括消息队列(Kafka、RabbitMQ、SQS)、数据库查询、外部监控系统、Cron 表达式等。KEDA 会根据事件源的指标自动计算所需的副本数(包括缩容到 0),然后通过 HPA 执行实际的扩缩容。B 错误:KEDA 不改变 Pod 调度算法。C 错误:KEDA 不替代 Pod 控制器,它只是提供指标和目标副本数。D 错误:KEDA 支持 Scale-to-Zero(将副本缩至 0),这是原生 HPA 做不到的。", + "source": null, + "related": [] + }, + { + "id": "sc-011", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "kubernetes", + "canary", + "istio" + ], + "question": "使用 Istio 实现基于 Header 的 Canary 路由时,以下 VirtualService 配置片段中,哪种方式能将带有 x-canary: true Header 的请求路由到新版本?", + "options": { + "A": "在 match 中设置 headers.x-canary.exact: \"true\",并在 route 的 destination.host 中指向新版本的 service", + "B": "在 DestinationRule 中设置 trafficPolicy.loadBalancer.simple: ROUND_ROBIN", + "C": "在 Gateway 中配置 match.headers.x-canary.exact: \"true\"", + "D": "在 VirtualService 的 route 中直接设置 weight: 100 并写死 Header 条件" + }, + "answer": "A", + "explanation": "Istio 的 VirtualService 中,可以通过 match.headers 指定请求匹配条件。具体来说,在 VirtualService 的 spec.http[].match[] 中设置 headers.x-canary.exact: \"true\",然后在对应的 route[] 中将 traffic 指向新版本的 Destination(如 svc-canary)。这样带有 x-canary: true Header 的请求会被路由到金丝雀版本,其他请求走主版本。B 错误:DestinationRule 的 loadBalancer 设置的是负载均衡策略,不是路由匹配。C 错误:Gateway 主要处理入口流量的 TLS 终止和主机匹配,Header 路由应在 VirtualService 中配置。D 错误:weight 和 match 条件是不同的配置维度,不可混用。", + "source": null, + "related": [] + }, + { + "id": "sc-012", + "type": "single_choice", + "difficulty": 5, + "tags": [ + "opentelemetry", + "collector", + "otlp" + ], + "question": "OpenTelemetry Collector 的 Pipeline 配置中,如果同时配置了 receivers.OTLP → processors.batch → exporters/prometheus 和 receivers.OTLP → processors.batch → exporters/logging,这意味着?", + "options": { + "A": "数据会被重复采集两次,导致指标翻倍", + "B": "数据会同时导出到 Prometheus 和标准输出(logging exporter),这是 Fan-out 模式,支持多目标导出", + "C": "只有第一个 exporter 能接收数据,第二个 exporter 会被忽略", + "D": "OTLP receiver 会根据数据类型自动路由:Metrics 到 Prometheus,Logs 到 logging" + }, + "answer": "B", + "explanation": "OpenTelemetry Collector 的 Pipeline 支持 Fan-out 模式,即一个 Pipeline 的 processor 链处理完毕后,可以将数据同时发送给多个 exporter。在这个配置中,数据经过 OTLP receiver 接收和 batch processor 处理后,会同时被发送到 Prometheus exporter(将指标暴露为 Prometheus 格式供抓取)和 logging exporter(将遥测数据输出到标准输出用于调试)。这是一个完全合法且常用的配置。A 错误:数据不会被重复采集,receiver 只采集一次,分发到多个 exporter。C 错误:所有配置的 exporter 都会接收数据,不存在被忽略的情况。D 错误:OTLP receiver 不做路由决策,Pipeline 配置才决定数据流向。", + "source": null, + "related": [] + }, + { + "id": "sc-013", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "prometheus", + "promql" + ], + "question": "在 PromQL 中,以下查询表达式 rate(http_requests_total{job=\"api\"}[5m]) 的含义是?", + "options": { + "A": "查询 5 分钟内 HTTP 请求的总数", + "B": "查询 HTTP 请求的每秒平均增长率(QPS),使用 5 分钟的时间窗口计算", + "C": "查询 5 分钟内 HTTP 请求的最大值", + "D": "查询 HTTP 请求的平均延迟时间" + }, + "answer": "B", + "explanation": "rate() 是 PromQL 中用于计算 Counter 类型指标每秒平均增长率的函数。rate(http_requests_total{job=\"api\"}[5m]) 的含义是:取最近 5 分钟窗口内 http_requests_total 的增量,除以时间间隔,得到每秒的平均请求速率(即 QPS)。[5m] 是 range vector selector,定义了计算的时间窗口。A 错误:查询总数用 sum(http_requests_total),而不是 rate。C 错误:rate 不是取最大值。D 错误:请求延迟通常由 histogram/summary 类型的指标记录,如 histogram_quantile()。", + "source": null, + "related": [] + }, + { + "id": "sc-014", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "hpa", + "vpa", + "cluster-autoscaler" + ], + "question": "在 Kubernetes 中同时使用 HPA 和 VPA 时需要注意什么?", + "options": { + "A": "HPA 和 VPA 完全独立,可以对同一指标(如 CPU)同时配置而不会冲突", + "B": "HPA 和 VPA 可以共存,但需要对相同的资源指标做互斥配置——如果 HPA 基于 CPU 扩缩 Pod 数量,VPA 不应再对 CPU 做垂直调整", + "C": "VPA 会自动替代 HPA 的功能,只需部署 VPA 即可", + "D": "HPA 和 VPA 不能在同一集群中使用,必须二选一" + }, + "answer": "B", + "explanation": "HPA 和 VPA 可以在同一集群中共存,但需要避免对相同指标的冲突配置。最佳实践是:HPA 负责基于某类指标(如 custom metrics 或 CPU)调整副本数,而 VPA 负责基于另一类指标(如 Memory)调整 Pod 的资源请求和限制。如果两者同时基于 CPU 调整,HPA 扩缩 Pod 数量会影响 CPU 的聚合指标,VPA 调整 CPU request 又会影响 HPA 的计算,形成冲突。官方建议使用 VPA 的 Off 模式(recommendation-only),由管理员手动或通过脚本应用推荐值,避免与 HPA 冲突。", + "source": null, + "related": [] + }, + { + "id": "sc-015", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "kubernetes", + "blue-green", + "rolling-update", + "grafana" + ], + "question": "在一个 Kubernetes 集群中,团队计划使用 Blue-Green 策略部署新版本应用。以下哪种方案是正确的 Blue-Green 实现方式?", + "options": { + "A": "使用单个 Deployment,通过修改镜像标签触发滚动更新,逐步替换旧版本 Pod", + "B": "同时运行两个 Deployment(blue 和 green),通过修改 Service 的 selector 指向新版本的 Deployment 来实现切换", + "C": "创建一个 DaemonSet 让新版本运行在所有节点上,然后删除旧版本的 Pod", + "D": "使用 StatefulSet 的分段发布(partition update)功能实现蓝绿切换" + }, + "answer": "B", + "explanation": "Blue-Green 部署的核心是同时维护两个完全相同的生产环境(blue 和 green)。新版本先部署到 green 环境(新 Deployment),测试验证后,通过修改 Service 的 selector label(如将 app: myapp, version: blue 改为 app: myapp, version: green)将流量一次性切换到新版本。回滚时只需将 selector 改回旧版本即可。A 错误:这是 Rolling Update 策略,不是 Blue-Green。C 错误:DaemonSet 用于在每个节点上运行一个 Pod,不适用于 Blue-Green 场景。D 错误:StatefulSet partition update 用于有状态应用的分段更新,不等同于 Blue-Green。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/interview-prep/k8s-observability/true_false.json b/topics/interview-prep/k8s-observability/true_false.json new file mode 100644 index 0000000..2499f76 --- /dev/null +++ b/topics/interview-prep/k8s-observability/true_false.json @@ -0,0 +1,161 @@ +{ + "topic": "k8s-observability", + "type": "true_false", + "schema_version": "1.0.0", + "generated": "2026-09-09T16:10:00+08:00", + "questions": [ + { + "id": "tf-001", + "type": "true_false", + "difficulty": 2, + "tags": [ + "kubernetes", + "deployment", + "HPA" + ], + "question": "HPA(Horizontal Pod Autoscaler)只能基于 CPU 使用率来自动扩缩 Pod 副本数,无法使用内存或其他自定义指标。", + "answer": false, + "explanation": "HPA 支持多种指标来源:默认的 CPU 和内存(通过 resource metrics)、自定义指标(custom metrics,如请求 QPS)、以及外部指标(external metrics,如 Kafka lag)。通过 metrics API 的扩展机制,HPA 几乎可以基于任何可量化的指标进行扩缩容。", + "source": null, + "related": [] + }, + { + "id": "tf-002", + "type": "true_false", + "difficulty": 3, + "tags": [ + "prometheus", + "grafana", + "opentelemetry" + ], + "question": "OpenTelemetry 的 Collector 架构采用 Receiver → Processor → Pipeline → Exporter 的管道模型,且支持同时向多个后端(如 Prometheus、Jaeger、Loki)导出数据。", + "answer": true, + "explanation": "OpenTelemetry Collector 的核心设计就是可插拔的管道架构:Receiver 负责接收遥测数据(OTLP、Jaeger、Prometheus 等格式),Processor 负责处理(批处理、过滤、属性修改等),Exporter 负责将数据推送到一个或多个后端。一个 Collector 实例可以配置多个 Pipeline 和多个 Exporter,实现数据扇出(fan-out)。", + "source": null, + "related": [] + }, + { + "id": "tf-003", + "type": "true_false", + "difficulty": 2, + "tags": [ + "kubernetes", + "liveness", + "readiness" + ], + "question": "Kubernetes 的 Liveness Probe 负责判断 Pod 是否准备好接收流量,失败时会将 Pod 从 Service 的 Endpoints 中移除。", + "answer": false, + "explanation": "这是 Readiness Probe 的职责。Liveness Probe 用于检测容器是否仍然存活(如是否发生死锁),失败时 kubelet 会重启容器。Readiness Probe 才负责判断 Pod 是否就绪,失败时会从 Service 的 Endpoints 列表中摘除,使流量不再路由到该 Pod。两者职责不同,不可混淆。", + "source": null, + "related": [] + }, + { + "id": "tf-004", + "type": "true_false", + "difficulty": 4, + "tags": [ + "kubernetes", + "deployment", + "rolling-update", + "HPA", + "VPA" + ], + "question": "在同一个 Deployment 上同时启用 HPA 和 VPA 是推荐的最佳实践,因为 HPA 负责副本数扩缩,VPA 负责单 Pod 资源调整,两者互补且无冲突。", + "answer": false, + "explanation": "HPA 和 VPA 同时作用于同一个 Deployment 时会产生冲突:HPA 根据资源使用率调整副本数,而 VPA 根据历史使用量调整 Pod 的 resource requests。当 VPA 提高 requests 时,HPA 感知到的使用率下降会触发缩容,形成振荡。社区推荐的模式是:HPA 使用自定义指标(如 QPS)而非 CPU/内存,让 VPA 只管资源配额。或者使用 VPA 的 Recommend 模式(仅给出建议,不自动修改)配合 HPA。", + "source": null, + "related": [] + }, + { + "id": "tf-005", + "type": "true_false", + "difficulty": 3, + "tags": [ + "kubernetes", + "pod", + "deployment" + ], + "question": "在 Kubernetes 中,DaemonSet 类型的控制器可以像 Deployment 一样通过修改 spec.replicas 字段来手动调整副本数。", + "answer": false, + "explanation": "DaemonSet 的设计目标是在每个(或指定的)节点上运行且仅运行一个 Pod 副本,因此它没有 spec.replicas 字段。DaemonSet 的 Pod 数量由集群中的节点数量决定,无法手动指定副本数。这与 Deployment、StatefulSet 等支持 replicas 字段的控制器有本质区别。", + "source": null, + "related": [] + }, + { + "id": "tf-006", + "type": "true_false", + "difficulty": 3, + "tags": [ + "prometheus", + "grafana" + ], + "question": "Prometheus 的 ServiceMonitor 资源直接指定 Pod 标签来发现抓取目标,因此即使没有创建 Service,只要 Pod 有正确标签就能被 Prometheus 自动抓取。", + "answer": false, + "explanation": "ServiceMonitor 是 Prometheus Operator 提供的 CRD,它的 spec.selector.matchLabels 匹配的是 Service 对象的标签,而非 Pod 的标签。匹配到的 Service 对应的 Endpoints(即背后的 Pod)才会被 Prometheus 抓取。因此,要让 Prometheus 通过 ServiceMonitor 抓取指标,必须先创建对应的 Service。如果想直接基于 Pod 标签抓取,应使用 PodMonitor 资源。", + "source": null, + "related": [] + }, + { + "id": "tf-007", + "type": "true_false", + "difficulty": 3, + "tags": [ + "kubernetes", + "rolling-update", + "deployment" + ], + "question": "Kubernetes Deployment 的 Rolling Update 策略中,maxSurge 和 maxUnavailable 这两个字段本质上是冗余的,因为其中一个参数就能完整控制滚动更新的行为。", + "answer": false, + "explanation": "maxSurge 和 maxUnavailable 控制的是滚动更新中两个不同维度:maxSurge 决定更新过程中最多可以比期望副本数多出多少 Pod(控制创建新 Pod 的速度),maxUnavailable 决定最多可以比期望副本数少多少 Pod(控制旧 Pod 被移除的速度)。两者共同约束更新过程中的可用容量。例如 maxSurge=25%、maxUnavailable=0 意味着必须先创建新 Pod 再销毁旧 Pod(零停机),而 maxSurge=0、maxUnavailable=25% 则先销毁旧 Pod 再创建新 Pod(有停机风险)。两者配合使用才能精确控制滚动更新的激进程度。", + "source": null, + "related": [] + }, + { + "id": "tf-008", + "type": "true_false", + "difficulty": 4, + "tags": [ + "opentelemetry", + "loki" + ], + "question": "OpenTelemetry Collector 在处理日志数据时,可以通过 Loki Exporter 将日志直接推送到 Grafana Loki,但 Loki 的核心索引机制是基于全文检索(类似 Elasticsearch),因此 OTLP 格式的日志需要先经过全文索引处理才能被高效查询。", + "answer": false, + "explanation": "Loki 的核心设计理念是「只索引标签,不索引日志内容」(like Prometheus, but for logs)。它只对标签(labels)建立索引,日志内容本身以压缩块的形式存储,在查询时才进行流式 grep。这与 Elasticsearch 的全文检索索引方式完全不同。OTLP 格式的日志通过 Loki Exporter 推送时,Resource Attributes 和 Log Records 的 Attributes 会被映射为 Loki 的标签,日志体直接存储,无需全文索引。这也是 Loki 相比 ELK 在存储成本上更有优势的原因。", + "source": null, + "related": [] + }, + { + "id": "tf-009", + "type": "true_false", + "difficulty": 4, + "tags": [ + "kubernetes", + "pod", + "liveness", + "readiness" + ], + "question": "为一个启动时间较长的应用(如大型 Java Spring Boot 服务)配置 Liveness Probe 时,应设置较短的 initialDelaySeconds(如 5 秒),以便尽早发现并重启不健康的容器。", + "answer": false, + "explanation": "对于启动时间较长的应用,initialDelaySeconds 设置过短会导致 Liveness Probe 在应用尚未完成初始化时就开始探测,kubelet 会误判容器不健康并反复重启,形成「重启风暴」。正确的做法是:(1) 使用 Startup Probe(K8s 1.18+)给应用充足的启动时间,Startup Probe 成功后 Liveness 和 Readiness Probe 才开始工作;(2) 如果不用 Startup Probe,应将 Liveness Probe 的 initialDelaySeconds 设置为大于应用的最长启动时间。另外,Liveness Probe 的超时时间和失败阈值也应留有余量,避免应用在 GC 或高负载时被误杀。", + "source": null, + "related": [] + }, + { + "id": "tf-010", + "type": "true_false", + "difficulty": 4, + "tags": [ + "kubernetes", + "canary", + "blue-green", + "service", + "ingress" + ], + "question": "在 Kubernetes 中实现 Canary 部署时,必须依赖 Istio 或 Linkerd 等服务网格才能按权重分流流量到新版本;仅使用原生的 Service 和 Ingress 资源无法实现流量的按比例分割。", + "answer": false, + "explanation": "虽然服务网格(Istio VirtualService、Linkerd TrafficSplit)提供了精细的流量管理能力,但原生 K8s 也能实现 Canary 分流:(1) 通过创建两个 Service(stable 和 canary)指向不同版本的 Deployment,配合 Ingress 的 annotations(如 Nginx Ingress 的 canary-weight annotation `nginx.ingress.kubernetes.io/canary-weight: 20`)实现按权重分流;(2) 使用 Deployment 的 Rolling Update 配合 minReadySeconds 和 progressDeadlineSeconds,让新旧版本 Pod 共存一段时间,Service 的 round-robin 负载均衡自然将部分流量导向新版本。原生方案虽然不如服务网格灵活,但对简单场景已经够用。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/interview-prep/message-queue/code_reading.json b/topics/interview-prep/message-queue/code_reading.json new file mode 100644 index 0000000..b7371a2 --- /dev/null +++ b/topics/interview-prep/message-queue/code_reading.json @@ -0,0 +1,303 @@ +{ + "topic": "message-queue", + "type": "code_reading", + "schema_version": "1.0.0", + "generated": "2026-09-09T16:00:00+08:00", + "questions": [ + { + "id": "cr-001", + "type": "code_reading", + "difficulty": 4, + "tags": [ + "message-queue", + "kafka", + "consumer-group", + "partition" + ], + "question": "阅读以下 Go 语言实现的 Kafka 消费者 Rebalance 监听器代码,分析其工作流程。", + "language": "go", + "code": "package kafka\n\nimport (\n \"log\"\n \"sync\"\n)\n\n// ConsumerRebalanceHandler 实现了 Kafka 消费者的 Rebalance 回调\n\ntype ConsumerRebalanceHandler struct {\n consumerGroup string\n partitions map[string][]int32 // topic -> partitions\n mu sync.RWMutex\n rebalanceCount int\n}\n\n// OnPartitionsAssigned 当分配到新分区时触发\nfunc (h *ConsumerRebalanceHandler) OnPartitionsAssigned(\n consumerGroup string,\n topicPartitions map[string][]int32,\n) {\n h.mu.Lock()\n defer h.mu.Unlock()\n \n // 1. 记录新分配的分区\n for topic, partitions := range topicPartitions {\n h.partitions[topic] = partitions\n }\n \n // 2. 重新初始化分区级别的消费位点缓存\n for topic, partitions := range topicPartitions {\n for _, p := range partitions {\n log.Printf(\"[ASSIGNED] group=%s topic=%s partition=%d\",\n consumerGroup, topic, p)\n }\n }\n \n h.rebalanceCount++\n log.Printf(\"Rebalance #%d 完成,共分配 %d 个分区\",\n h.rebalanceCount, countPartitions(topicPartitions))\n}\n\n// OnPartitionsRevoked 当分区被回收时触发\nfunc (h *ConsumerRebalanceHandler) OnPartitionsRevoked(\n consumerGroup string,\n topicPartitions map[string][]int32,\n) {\n h.mu.Lock()\n defer h.mu.Unlock()\n \n // 1. 先提交当前消费位点(确保不丢消息)\n for topic, partitions := range topicPartitions {\n for _, p := range partitions {\n log.Printf(\"[REVOKED] 提交位点: group=%s topic=%s partition=%d\",\n consumerGroup, topic, p)\n }\n }\n \n // 2. 清理分区状态\n for topic := range topicPartitions {\n delete(h.partitions, topic)\n }\n}\n\n// OnPartitionsLost 当分区不可用时触发(比 Revoked 更激进)\nfunc (h *ConsumerRebalanceHandler) OnPartitionsLost(\n consumerGroup string,\n topicPartitions map[string][]int32,\n) {\n // 不提交位点,直接清理\n h.mu.Lock()\n defer h.mu.Unlock()\n \n for topic := range topicPartitions {\n delete(h.partitions, topic)\n }\n log.Printf(\"[LOST] 分区丢失,未提交位点\")\n}", + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "在 OnPartitionsRevoked 回调中,为什么要先提交消费位点再清理分区状态?", + "options": { + "A": "为了提高吞吐量,将位点提交和状态清理合并执行", + "B": "确保在分区被其他消费者接管前,当前已消费的进度不会丢失", + "C": "Kafka 协议要求在 revoke 时必须提交位点,否则会报错", + "D": "为了清理 Broker 端的位点缓存,释放存储空间" + }, + "answer": "B", + "explanation": "Rebalance 的本质是将分区重新分配给其他消费者。如果不先提交位点就清理状态,新的消费者将从上次提交的位点开始消费,导致已处理的消息被重复消费。先提交位点可以保证新消费者从正确的位置继续,避免消息丢失或大量重复。Kafka 协议本身并不强制要求 revoke 时提交位点(选项 C 错误),这是一个最佳实践。" + }, + { + "index": 2, + "type": "single_choice", + "question": "OnPartitionsLost 与 OnPartitionsRevoked 的核心区别是什么?", + "options": { + "A": "OnPartitionsLost 会触发更重的 Rebalance 流程", + "B": "OnPartitionsLost 不提交位点,适用于分区分配异常丢失的场景;OnPartitionsRevoked 是正常的 Rebalance 流程", + "C": "两者功能完全相同,只是调用时机不同", + "D": "OnPartitionsLost 是客户端行为,OnPartitionsRevoked 是服务端行为" + }, + "answer": "B", + "explanation": "在 Kafka 的 Cooperative Rebalance 协议中,OnPartitionsLost 表示分区被异常剥夺(如 Consumer 挂了但 session 超时未过),此时无法安全提交位点(可能已在别处提交),所以直接清理状态。OnPartitionsRevoked 是正常的 Rebalance 流程,消费者有机会提交位点后再释放分区。代码中 OnPartitionsLost 也注释了「未提交位点」,印证了这一设计意图。" + }, + { + "index": 3, + "type": "short_answer", + "question": "代码中使用 sync.RWMutex 而非 sync.Mutex 的原因是什么?请结合消费场景分析其性能优势。", + "answer": "OnPartitionsAssigned 和 OnPartitionsRevoked 只在 Rebalance 发生时被调用(低频),而消费者主线程会频繁读取 h.partitions 来判断某分区是否仍由自己负责(高频)。RWMutex 允许多个读操作并发执行,只有写操作(Rebalance 回调)才需要独占锁,因此用 RWMutex 可以避免高频读操作之间的锁竞争,显著提升正常消费路径的吞吐量。", + "keywords": [ + "读写锁", + "高频读", + "低频写", + "Rebalance", + "消费主线程", + "并发" + ], + "scoring_rubric": "答出「Rebalance 回调低频写、消费路径高频读」的核心场景差异得 3 分;提到 RWMutex 允许并发读得 2 分;提到避免锁竞争或性能提升得 1 分。满分 6 分,4 分及以上通过。" + } + ], + "explanation": "本题考查 Kafka 消费者 Rebalance 机制的核心代码逻辑。Rebalance 是 Consumer Group 模型的关键机制,当消费者加入或离开组时,分区会被重新分配。理解三个回调(Assigned/Revoked/Lost)的调用时机和职责差异,是正确实现消费者的基础。重点掌握:(1) Revoked 时先提交位点防止消息重复;(2) Lost 与 Revoked 的语义差异;(3) 并发控制在消费路径中的应用。", + "source": null, + "related": [] + }, + { + "id": "cr-002", + "type": "code_reading", + "difficulty": 5, + "tags": [ + "message-queue", + "rocketmq", + "transactional-message", + "durable" + ], + "question": "阅读以下 Java 代码,分析 RocketMQ 事务消息的发送与回查机制实现。", + "language": "java", + "code": "package com.example.mq.transaction;\n\nimport org.apache.rocketmq.client.producer.*;\nimport org.apache.rocketmq.common.message.*;\n\npublic class TransactionMessageService {\n \n private final TransactionMQProducer producer;\n \n public TransactionMessageService(String namesrvAddr) {\n this.producer = new TransactionMQProducer(\"tx-producer-group\");\n this.producer.setNamesrvAddr(namesrvAddr);\n this.producer.setTransactionListener(new OrderTransactionListener());\n this.producer.setCheckThreadPool(5);\n this.producer.setCheckThreadPoolMaxSize(10);\n }\n \n // 发送事务消息\n public TransactionSendResult sendOrderMessage(Order order) {\n Message msg = new Message(\n \"ORDER_TOPIC\", // topic\n \"OrderTag\", // tag\n order.getOrderId(), // keys(用于回查时定位本地事务)\n order.toJson().getBytes() // body\n );\n \n // 附加业务上下文,供本地事务使用\n msg.putUserProperty(\"bizType\", \"ORDER_CREATE\");\n msg.putUserProperty(\"amount\", String.valueOf(order.getAmount()));\n \n // 发送半消息(half message),消费者此时不可见\n TransactionSendResult result = producer.sendMessageInTransaction(msg, order);\n \n log.info(\"事务消息发送结果: txId={}, localState={}, sendStatus={}\",\n result.getTransactionId(),\n result.getLocalTransactionState(),\n result.getSendStatus());\n \n return result;\n }\n}\n\n// 事务监听器:实现本地事务 + 回查逻辑\nclass OrderTransactionListener implements TransactionListener {\n \n private final OrderService orderService;\n \n @Override\n public LocalTransactionState executeLocalTransaction(Message msg, Object arg) {\n Order order = (Order) arg;\n try {\n // 1. 执行本地事务(创建订单)\n orderService.createOrder(order);\n // 2. 本地事务成功,提交半消息 → 消费者可见\n return LocalTransactionState.COMMIT_MESSAGE;\n } catch (DuplicateKeyException e) {\n // 3. 订单已存在(幂等),直接提交\n return LocalTransactionState.COMMIT_MESSAGE;\n } catch (Exception e) {\n // 4. 本地事务失败,回滚半消息 → 消费者不可见\n return LocalTransactionState.ROLLBACK_MESSAGE;\n }\n }\n \n @Override\n public LocalTransactionState checkLocalTransaction(MessageExt msg) {\n String orderId = msg.getKeys();\n \n // 回查逻辑:根据 orderId 查询本地事务状态\n Order order = orderService.queryOrder(orderId);\n \n if (order == null) {\n // 查不到 → 本地事务可能未执行或已回滚\n return LocalTransactionState.UNKNOW;\n }\n \n if (\"CREATED\".equals(order.getStatus()) || \"COMPLETED\".equals(order.getStatus())) {\n return LocalTransactionState.COMMIT_MESSAGE;\n }\n \n if (\"CANCELLED\".equals(order.getStatus())) {\n return LocalTransactionState.ROLLBACK_MESSAGE;\n }\n \n return LocalTransactionState.UNKNOW;\n }\n}", + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "当 executeLocalTransaction 抛出异常导致返回 ROLLBACK_MESSAGE 时,以下描述正确的是?", + "options": { + "A": "Broker 会将半消息标记为已回滚,消费者不会收到该消息,但消息仍会保留在 COMMIT_LOG 中", + "B": "Broker 会立即删除该半消息的物理存储", + "C": "Broker 会将该消息转发到死信队列供人工处理", + "D": "Broker 会自动重试 executeLocalTransaction 三次" + }, + "answer": "A", + "explanation": "RocketMQ 事务消息回滚时,Broker 只是将半消息标记为回滚状态(在 HALF_TOPIC 中),使其对消费者不可见。消息的物理数据仍保留在 COMMIT_LOG 中(RocketMQ 采用追加写入,不支持物理删除),等待后续日志清理机制统一回收。选项 B 错误,RocketMQ 不做物理删除;选项 C 错误,回滚不是死信;选项 D 错误,回滚后不会重试本地事务。" + }, + { + "index": 2, + "type": "short_answer", + "question": "当 checkLocalTransaction 返回 UNKNOW 时,RocketMQ Broker 的行为是什么?为什么要设计 UNKNOW 状态?", + "answer": "当 checkLocalTransaction 返回 UNKNOW 时,Broker 不会对半消息做任何操作(既不提交也不回滚),而是等待一段时间后再次发起回查。RocketMQ Broker 默认会以递增的时间间隔(如 60s, 120s, 240s...)最多回查 15 次。设计 UNKNOW 状态的原因是:本地事务可能正在执行中(如分布式事务协调器尚未完成),此时查询结果是不确定的,需要延迟再次确认。如果直接 COMMIT 或 ROLLBACK 可能导致数据不一致。", + "keywords": [ + "UNKNOW", + "延迟回查", + "递增间隔", + "数据一致性", + "不确定性" + ], + "scoring_rubric": "答出「Broker 会等待后再次回查」得 2 分;答出「递增间隔/多次回查」得 2 分;答出「UNKNOW 用于处理不确定状态/事务进行中」得 2 分。满分 6 分,4 分及以上通过。" + }, + { + "index": 3, + "type": "single_choice", + "question": "代码中使用 order.getOrderId() 作为消息的 keys,这一设计对回查机制有什么关键作用?", + "options": { + "A": "仅用于消息检索,与回查机制无关", + "B": "回查时 Broker 将 keys 作为 MessageExt 的一部分下发,客户端通过 keys 定位对应的本地事务记录", + "C": "keys 会被 Broker 用于自动路由到正确的 Broker 节点", + "D": "keys 是消息的唯一标识,用于去重" + }, + "answer": "B", + "explanation": "在 checkLocalTransaction 回调中,msg.getKeys() 返回的就是发送时设置的 keys 值(即 orderId)。回查的本质是 Broker 告诉 Producer「我有一个半消息不确定状态,请查一下你的本地事务」,Producer 需要一个标识来找到对应的本地记录。如果 keys 为空或不可唯一标识事务,回查时就无法判断本地事务是否成功。这正是代码中用 orderId 作为 keys 的核心原因。" + } + ], + "explanation": "本题考查 RocketMQ 事务消息的完整生命周期:半消息发送 → executeLocalTransaction → 回查机制。事务消息的核心在于「本地事务 + 消息投递」的原子性保证。理解三种 LocalTransactionState(COMMIT/ROLLBACK/UNKNOW)的语义和 Broker 侧行为,以及 keys 在回查链路中的桥梁作用,是正确实现事务消息的关键。", + "source": null, + "related": [] + }, + { + "id": "cr-003", + "type": "code_reading", + "difficulty": 4, + "tags": [ + "message-queue", + "dead-letter-queue", + "durable" + ], + "question": "阅读以下 Go 语言实现的死信队列消费处理逻辑代码,分析其重试与兜底策略。", + "language": "go", + "code": "package mq\n\nimport (\n \"context\"\n \"encoding/json\"\n \"fmt\"\n \"log\"\n \"time\"\n)\n\n// DeadLetterHandler 处理死信队列中的消息\n\ntype DeadLetterHandler struct {\n retryProducer Producer\n alertService AlertService\n maxRetries int\n retryDelayBase time.Duration\n}\n\n// DeadLetterMessage 死信消息的结构\ntype DeadLetterMessage struct {\n OriginalTopic string `json:\"original_topic\"`\n OriginalBody []byte `json:\"original_body\"`\n ErrorInfo ErrorInfo `json:\"error_info\"`\n RetryCount int `json:\"retry_count\"`\n DLQTimestamp time.Time `json:\"dlq_timestamp\"`\n Metadata map[string]string `json:\"metadata\"`\n}\n\ntype ErrorInfo struct {\n ErrorCode string `json:\"error_code\"`\n Message string `json:\"message\"`\n}\n\n// HandleDeadLetter 处理单条死信消息\nfunc (h *DeadLetterHandler) HandleDeadLetter(ctx context.Context, msg *Message) error {\n var dlqMsg DeadLetterMessage\n if err := json.Unmarshal(msg.Body, &dlqMsg); err != nil {\n log.Printf(\"死信消息解析失败,丢弃: %v\", err)\n return nil // 解析失败的消息无法处理,确认消费避免无限循环\n }\n \n // 策略1:可重试的错误 → 重新投递到原 topic\n if h.isRetryable(dlqMsg.ErrorInfo.ErrorCode) {\n if dlqMsg.RetryCount < h.maxRetries {\n delay := h.calculateDelay(dlqMsg.RetryCount)\n \n err := h.retryProducer.SendWithDelay(\n ctx,\n dlqMsg.OriginalTopic,\n dlqMsg.OriginalBody,\n delay,\n )\n if err != nil {\n return fmt.Errorf(\"重试投递失败: %w\", err)\n }\n \n log.Printf(\"死信消息重试投递: topic=%s retry=%d/%d delay=%s\",\n dlqMsg.OriginalTopic, dlqMsg.RetryCount+1, h.maxRetries, delay)\n return nil\n }\n // 重试次数用尽,降级到告警\n log.Printf(\"重试次数用尽: topic=%s retry=%d\",\n dlqMsg.OriginalTopic, dlqMsg.RetryCount)\n }\n \n // 策略2:不可重试或重试用尽 → 告警 + 人工处理队列\n err := h.alertService.SendAlert(AlertPayload{\n Level: \"CRITICAL\",\n Title: \"死信消息需人工处理\",\n Detail: fmt.Sprintf(\"topic=%s error=%s retry=%d\",\n dlqMsg.OriginalTopic, dlqMsg.ErrorInfo.Message, dlqMsg.RetryCount),\n Timestamp: time.Now(),\n })\n if err != nil {\n log.Printf(\"告警发送失败: %v\", err)\n }\n \n // 将消息投递到人工处理队列\n return h.retryProducer.SendToQueue(ctx, \"MANUAL_DLQ_TOPIC\", msg.Body)\n}\n\nfunc (h *DeadLetterHandler) isRetryable(errorCode string) bool {\n retryableCodes := map[string]bool{\n \"NETWORK_TIMEOUT\": true,\n \"SERVICE_UNAVAILABLE\": true,\n \"RESOURCE_EXHAUSTED\": true,\n \"INVALID_INPUT\": false,\n \"PERMISSION_DENIED\": false,\n \"BUSINESS_RULE_VIOLATION\": false,\n }\n return retryableCodes[errorCode]\n}\n\nfunc (h *DeadLetterHandler) calculateDelay(retryCount int) time.Duration {\n // 指数退避:base * 2^retryCount,上限 30 分钟\n delay := h.retryDelayBase * time.Duration(1< maxDelay {\n delay = maxDelay\n }\n return delay\n}", + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "当死信消息解析失败(JSON Unmarshal 返回错误)时,代码选择返回 nil 而非 error,这样做的原因是?", + "options": { + "A": "解析失败的消息由上游负责重试,当前层不需要关心", + "B": "返回 error 会导致消息被重新投递到死信队列,形成无限循环", + "C": "解析失败意味着消息格式损坏,返回 error 会让 Broker 丢弃该消息", + "D": "这是一种错误处理的简化写法,功能上没有区别" + }, + "answer": "B", + "explanation": "在消息队列的消费模型中,消费失败(返回 error)通常会导致消息被重试或重新投递到死信队列。对于一条格式已损坏的死信消息,如果返回 error,它会被再次投递到死信队列,再次解析失败,形成无限循环。返回 nil 意味着「我已成功处理(虽然实际上是放弃)」,确认消费后消息不会被重投。这是处理「无法挽救的消息」的标准防御性编程模式。" + }, + { + "index": 2, + "type": "single_choice", + "question": "calculateDelay 使用指数退避算法 `base * 2^retryCount` 并设置 30 分钟上限,这样设计的主要目的是?", + "options": { + "A": "减少 Broker 的存储压力", + "B": "避免对下游服务产生突发重试风暴,同时保证问题恢复后消息仍能被及时处理", + "C": "节省 Producer 的网络带宽", + "D": "满足 Kafka 的消息保留策略要求" + }, + "answer": "B", + "explanation": "指数退避(Exponential Backoff)的核心目的是在「给下游服务恢复时间」和「问题修复后及时重试」之间取得平衡。如果以固定间隔重试,当下游服务不可用时会产生大量无效请求(重试风暴);如果退避时间过长,服务恢复后消息处理会有不必要的延迟。设置 30 分钟上限确保即使重试多次,延迟也不会过长,保证业务时效性。" + }, + { + "index": 3, + "type": "short_answer", + "question": "请分析该死信处理逻辑的三层兜底策略,并说明每层分别解决什么问题。", + "answer": "三层兜底策略:(1) 可重试错误 + 未超限 → 指数退避重投原 topic,解决临时性故障(网络超时、服务不可用等)导致的消息消费失败,给下游恢复机会;(2) 可重试但重试用尽 / 不可重试错误 → 发送告警通知运维人员,解决需要人工介入的故障场景(如业务规则冲突、权限问题等);(3) 最终兜底 → 投递到 MANUAL_DLQ_TOPIC 人工处理队列,确保消息不会丢失,为人工修复后重新处理提供入口。", + "keywords": [ + "重试投递", + "告警", + "人工处理队列", + "指数退避", + "三层兜底", + "不可重试错误" + ], + "scoring_rubric": "答出三层结构(重试/告警/人工队列)各得 2 分;能说明每层解决的问题类型(临时故障/人工介入/最终兜底)得 2 分。满分 8 分,5 分及以上通过。" + } + ], + "explanation": "本题考查死信队列消费处理的核心设计模式。死信消息是消费失败的最终归宿,如何合理处理决定了系统的可靠性和可运维性。关键设计点包括:(1) 解析失败的防御性处理(避免无限循环);(2) 可重试/不可重试错误的分类策略;(3) 指数退避重试机制;(4) 告警与人工处理的最终兜底。这套模式在 Kafka、RocketMQ、RabbitMQ 等消息系统中普遍适用。", + "source": null, + "related": [] + }, + { + "id": "cr-004", + "type": "code_reading", + "difficulty": 3, + "tags": [ + "message-queue", + "partition", + "cluster" + ], + "question": "阅读以下 Java 代码,分析 Kafka 生产者自定义分区路由策略的实现逻辑。", + "language": "java", + "code": "package com.example.mq.partitioner;\n\nimport org.apache.kafka.clients.producer.Partitioner;\nimport org.apache.kafka.common.Cluster;\nimport org.apache.kafka.common.PartitionInfo;\nimport java.util.*;\nimport java.util.concurrent.ConcurrentHashMap;\n\n/**\n * 基于用户 ID 一致性哈希的分区策略\n * 保证同一用户的消息始终路由到同一分区,实现分区级顺序性\n */\npublic class ConsistentHashPartitioner implements Partitioner {\n \n private final Map topicRings = new ConcurrentHashMap<>();\n private final int virtualNodes = 150; // 每个物理分区的虚拟节点数\n \n @Override\n public void configure(Map configs) {\n // 可从配置中读取虚拟节点数\n Object vnode = configs.get(\"consistent.hash.virtual.nodes\");\n if (vnode instanceof Integer) {\n // 此处省略赋值\n }\n }\n \n @Override\n public int partition(String topic, Object key, byte[] keyBytes,\n Object value, byte[] valueBytes, Cluster cluster) {\n List partitions = cluster.partitionsForTopic(topic);\n int numPartitions = partitions.size();\n \n if (key == null || keyBytes == null) {\n // 无 key 时使用默认轮询策略\n return defaultPartition(topic, numPartitions);\n }\n \n // 获取或创建该 topic 的哈希环\n ConsistentHashRing ring = topicRings.computeIfAbsent(topic,\n t -> buildHashRing(partitions));\n \n // 使用 murmur3 哈希确定路由\n int partition = ring.getNode(keyBytes);\n \n // 兜底:确保返回有效的分区编号\n return partition % numPartitions;\n }\n \n private ConsistentHashRing buildHashRing(List partitions) {\n ConsistentHashRing ring = new ConsistentHashRing();\n for (PartitionInfo info : partitions) {\n ring.addPhysicalNode(info.partition(), virtualNodes);\n }\n return ring;\n }\n \n @Override\n public void close() {\n topicRings.clear();\n }\n \n private int defaultPartition(String topic, int numPartitions) {\n return Thread.currentThread().getId() % numPartitions;\n }\n}\n\n// 一致性哈希环实现\nclass ConsistentHashRing {\n private final TreeMap ring = new TreeMap<>();\n \n void addPhysicalNode(int nodeId, int virtualNodes) {\n for (int i = 0; i < virtualNodes; i++) {\n long hash = hash(nodeId + \"#\" + i);\n ring.put(hash, nodeId);\n }\n }\n \n int getNode(byte[] key) {\n long hash = hash(key);\n Map.Entry entry = ring.ceilingEntry(hash);\n if (entry == null) {\n entry = ring.firstEntry(); // 环形回绕\n }\n return entry.getValue();\n }\n \n private long hash(byte[] data) {\n // MurmurHash3 32-bit, 取绝对值确保非负\n return Math.abs(MurmurHash3.hash32(data));\n }\n}", + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "当 Broker 发生扩缩容(分区数变化)时,一致性哈希环相比普通取模分区的优势是什么?", + "options": { + "A": "只有约 1/N 的消息需要重新路由到新分区,N 为分区总数,减少消息重平衡范围", + "B": "扩缩容时不会有任何消息路由变化,完全无缝", + "C": "扩缩容时所有消息都会重新路由,但延迟更低", + "D": "一致性哈希环不支持分区数变化" + }, + "answer": "A", + "explanation": "一致性哈希的核心优势在于:当节点数从 N 变为 N+1 时,虚拟节点环上的映射关系只影响约 1/(N+1) 的数据路由。相比普通取模分区(hash % N)在 N 变化时几乎所有 key 的分区都可能改变,一致性哈希大大减少了路由变化的范围,降低了扩缩容对消费者的影响(已消费的位点无需大量重算)。选项 B 错误,任何哈希策略在分区变化时都无法完全避免路由变化。" + }, + { + "index": 2, + "type": "single_choice", + "question": "代码中为每个物理分区创建 150 个虚拟节点,虚拟节点的作用是什么?", + "options": { + "A": "增加网络连接数,提高消息吞吐量", + "B": "使消息在各物理分区间的分布更加均匀,减少数据倾斜", + "C": "提供消息备份,一个分区不可用时自动切换到虚拟节点", + "D": "用于实现消息的延迟投递" + }, + "answer": "B", + "explanation": "物理分区数较少时(如 3-6 个),直接用物理节点构建哈希环会导致数据分布不均匀(某些分区承担过多路由)。虚拟节点通过为每个物理分区创建多个哈希映射点,使哈希环上的节点分布更密集、更均匀,从而让消息负载均衡到各个物理分区。150 个虚拟节点是常用的经验值,可以在均匀性和查找效率之间取得平衡。" + }, + { + "index": 3, + "type": "short_answer", + "question": "代码中 defaultPartition 使用 Thread.currentThread().getId() % numPartitions 作为无 key 消息的分区策略,这种方式存在什么潜在问题?", + "answer": "这种无 key 时的分区策略存在以下问题:(1) 线程 ID 不一定均匀分布,可能导致分区倾斜——某些线程 ID 哈希后集中在少数分区;(2) 线程池大小变化(如扩容后线程数增加)会导致同一批消息的路由分布发生变化;(3) 与消费者组的线程模型耦合,如果消费者端线程数变化,生产端的分区分布也受影响。更稳健的做法是使用 AtomicInteger 的自增计数器取模,确保严格的轮询均匀分布。", + "keywords": [ + "线程ID不均匀", + "分区倾斜", + "线程池变化", + "轮询", + "AtomicInteger" + ], + "scoring_rubric": "答出「线程ID分布不均导致分区倾斜」得 3 分;答出「线程数变化影响路由分布」得 2 分;提出改进建议(如 AtomicInteger 轮询)得 1 分。满分 6 分,4 分及以上通过。" + } + ], + "explanation": "本题考查 Kafka 自定义分区策略的设计与实现。Kafka 的默认分区策略(轮询或 key hash)在很多场景下不够灵活,通过实现 Partitioner 接口可以自定义路由逻辑。一致性哈希分区策略是保证「同一业务实体的消息顺序性」的经典方案,同时在扩缩容时具有良好的稳定性。理解虚拟节点、哈希环回绕、无 key 降级策略等细节是正确实现的前提。", + "source": null, + "related": [] + }, + { + "id": "cr-005", + "type": "code_reading", + "difficulty": 3, + "tags": [ + "message-queue", + "consumer-group", + "durable" + ], + "question": "阅读以下 Go 语言实现的消息幂等消费去重逻辑代码,分析其去重机制。", + "language": "go", + "code": "package mq\n\nimport (\n \"context\"\n \"crypto/sha256\"\n \"encoding/hex\"\n \"fmt\"\n \"log\"\n \"time\"\n)\n\n// IdempotentConsumer 幂等消费者\ntype IdempotentConsumer struct {\n store DeduplicationStore\n processor MessageProcessor\n windowSize time.Duration // 去重时间窗口\n}\n\ntype DeduplicationStore interface {\n // SetIfAbsent 原子操作:若 key 不存在则设置并返回 true,否则返回 false\n SetIfAbsent(ctx context.Context, key string, ttl time.Duration) (bool, error)\n // Get 获取去重记录的存在时间\n Get(ctx context.Context, key string) (time.Time, error)\n}\n\n// ConsumeMessage 幂等消费入口\nfunc (c *IdempotentConsumer) ConsumeMessage(ctx context.Context, msg *Message) error {\n // 1. 生成幂等键\n dedupKey := c.buildDedupKey(msg)\n \n // 2. 原子性检查 + 写入\n firstTime, err := c.store.SetIfAbsent(ctx, dedupKey, c.windowSize)\n if err != nil {\n // 存储层故障时的降级策略:放行,不做去重\n log.Printf(\"去重存储故障,降级放行: key=%s err=%v\", dedupKey, err)\n return c.processor.Process(ctx, msg)\n }\n \n if !firstTime {\n // 3. 重复消息,跳过处理\n log.Printf(\"重复消息已跳过: key=%s msgId=%s\", dedupKey, msg.ID)\n return nil\n }\n \n // 4. 首次消息,正常处理\n err = c.processor.Process(ctx, msg)\n if err != nil {\n // 5. 处理失败时需要删除去重键,允许重试\n c.store.Delete(ctx, dedupKey)\n return fmt.Errorf(\"消息处理失败,已清除去重键: %w\", err)\n }\n \n log.Printf(\"消息处理成功: key=%s msgId=%s\", dedupKey, msg.ID)\n return nil\n}\n\n// buildDedupKey 构造去重键:组合业务维度,避免跨业务误去重\nfunc (c *IdempotentConsumer) buildDedupKey(msg *Message) string {\n // 方案A:使用消息内置的唯一 ID(最简单)\n if msg.ID != \"\" {\n return fmt.Sprintf(\"dedup:%s\", msg.ID)\n }\n // 方案B:使用业务字段哈希(适用于无全局唯一 ID 的场景)\n raw := fmt.Sprintf(\"%s:%s:%s:%d\",\n msg.Headers[\"biz_type\"],\n msg.Headers[\"order_id\"],\n msg.Headers[\"action\"],\n msg.Timestamp.Unix(),\n )\n hash := sha256.Sum256([]byte(raw))\n return fmt.Sprintf(\"dedup:hash:%s\", hex.EncodeToString(hash[:8]))\n}", + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "当 SetIfAbsent 返回 false(非首次)时,代码直接返回 nil 不做任何处理。如果此时消费者需要「重新处理失败的消息」(即重试场景),这种设计会带来什么问题?", + "options": { + "A": "会导致消息被重复消费", + "B": "会导致重试的失败消息永远无法被重新处理,即「吞掉」重试消息", + "C": "没有任何问题,这是正确的去重逻辑", + "D": "会导致去重键永久占用存储空间" + }, + "answer": "B", + "explanation": "这段代码的去重逻辑是「一次成功,永久跳过」。如果消息第一次处理失败(Process 返回 error),代码会删除去重键允许重试。但如果消息第一次处理成功后,由于某种原因需要重试处理(如业务层面的补偿),去重键仍然存在,重试消息会被当作重复消息跳过。这是去重逻辑的权衡——在「防重复」和「允许重试」之间需要根据业务场景选择不同的策略(如引入版本号或状态字段)。" + }, + { + "index": 2, + "type": "single_choice", + "question": "当去重存储(Redis 等)故障时,代码选择「降级放行」而非「消费失败重试」,这样设计的理由是什么?", + "options": { + "A": "存储故障是永久性的,重试也没有意义", + "B": "保证消息处理的可用性优先于去重的严格性,避免因去重组件故障导致整体消费停滞", + "C": "Kafka 不支持消息重试", + "D": "降级放行的代码实现更简单" + }, + "answer": "B", + "explanation": "分布式系统中,去重存储(如 Redis)是外部依赖,可能随时故障。如果此时消费失败重试,消息会被反复投递但始终无法处理,导致消费积压甚至消费者组崩溃。降级放行意味着「宁可重复消费,也不阻塞消费」,将可用性放在首位。重复消费的代价(如多扣一次款)可以通过业务层幂等(如数据库唯一约束)来兜底。这是分布式系统中「fail-open vs fail-close」的经典权衡。" + }, + { + "index": 3, + "type": "short_answer", + "question": "代码中 buildDedupKey 提供了两种方案:消息 ID 和业务字段哈希。请分析各自的适用场景和优缺点。", + "answer": "方案 A(消息 ID):适用场景是消息系统提供了全局唯一 ID(如 Kafka 的 msg.Id 或 RocketMQ 的 MsgKey)。优点是简单可靠、无哈希冲突风险;缺点是依赖消息系统的 ID 唯一性保证,且不同消息系统或重试场景可能生成不同 ID,导致去重失效。\n方案 B(业务字段哈希):适用场景是消息没有全局唯一 ID,或需要按业务语义去重(如同一订单的同一操作)。优点是去重粒度可由业务控制,跨重试和消息系统保持一致;缺点是需要选择正确的业务字段组合,哈希存在理论上的碰撞风险,且时间精度(Unix秒级)可能影响去重精度。", + "keywords": [ + "消息ID", + "业务字段哈希", + "全局唯一", + "去重粒度", + "哈希碰撞", + "适用场景" + ], + "scoring_rubric": "方案 A 分析得当(适用场景 + 优缺点)得 3 分;方案 B 分析得当得 3 分;对比分析清晰得 1 分。满分 7 分,4 分及以上通过。" + } + ], + "explanation": "本题考查消息幂等消费的核心设计模式。在分布式消息系统中,at-least-once 语义保证消息不丢但可能重复,幂等消费是处理重复消息的关键机制。核心设计点包括:(1) 原子性的去重检查(SetIfAbsent);(2) 去重键的设计(消息 ID vs 业务字段);(3) 存储故障的降级策略;(4) 处理失败时去重键的清理。在实际生产中,去重通常结合消息端(去重存储)和业务端(数据库唯一约束)双重保障。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/interview-prep/message-queue/fill_blank.json b/topics/interview-prep/message-queue/fill_blank.json new file mode 100644 index 0000000..5baf323 --- /dev/null +++ b/topics/interview-prep/message-queue/fill_blank.json @@ -0,0 +1,189 @@ +{ + "topic": "message-queue", + "type": "fill_blank", + "schema_version": "1.0.0", + "generated": "2026-09-09T16:12:31+08:00", + "questions": [ + { + "id": "fb-001", + "type": "fill_blank", + "difficulty": 1, + "tags": [ + "kafka", + "durable" + ], + "question": "Kafka 中消息被追加到 Topic 的______末尾,这种写入方式被称为______写。", + "answer": [ + "partition", + "append-only" + ], + "answer_rule": "all", + "explanation": "Kafka 中 Topic 是逻辑概念,实际数据存储在 Partition(分区)中。每个 Partition 本质上是一个有序的、不可变的消息序列,新消息只能追加到末尾(append-only),这种顺序写方式极大减少了磁盘寻址开销,是 Kafka 高吞吐量的核心设计之一。", + "source": null, + "related": [] + }, + { + "id": "fb-002", + "type": "fill_blank", + "difficulty": 2, + "tags": [ + "rocketmq", + "transactional-message" + ], + "question": "RocketMQ 事务消息采用______消息机制:先发送半消息(Half Message)给 Broker,本地事务执行成功后再发送______(Commit/Rollback)指令,Broker 据此决定是否投递该消息。", + "answer": [ + "two-phase", + "commit" + ], + "answer_rule": "all", + "explanation": "RocketMQ 的事务消息基于两阶段提交思想。第一阶段发送 Half Message,此时消息对消费者不可见;第二阶段根据本地事务执行结果发送 Commit(提交,消息投递)或 Rollback(回滚,消息丢弃)。若 Broker 长时间未收到第二阶段指令,会主动回查生产者本地事务状态,确保最终一致性。", + "source": null, + "related": [] + }, + { + "id": "fb-003", + "type": "fill_blank", + "difficulty": 2, + "tags": [ + "kafka", + "durable", + "zero-copy" + ], + "question": "Kafka 利用操作系统的______系统调用实现零拷贝技术,将磁盘文件数据直接传输到网络 Socket,避免了用户态与内核态之间的数据拷贝。", + "answer": [ + "sendfile" + ], + "answer_rule": "all", + "explanation": "零拷贝(Zero-Copy)是 Kafka 高性能消费的关键技术。传统 I/O 需要经历:磁盘→内核缓冲区→用户缓冲区→内核 Socket 缓冲区→网卡,涉及多次拷贝和上下文切换。Kafka 通过 sendfile 系统调用,让数据直接从 PageCache 传输到网卡,仅需一次内核态操作,显著降低了消费延迟和 CPU 开销。", + "source": null, + "related": [] + }, + { + "id": "fb-004", + "type": "fill_blank", + "difficulty": 2, + "tags": [ + "dead-letter-queue" + ], + "question": "当消费者连续消费某条消息失败超过最大重试次数后,该消息会被投递到______队列,以避免阻塞后续正常消息的消费。", + "answer": [ + "dead-letter", + "死信" + ], + "answer_rule": "any", + "explanation": "死信队列(Dead Letter Queue,DLQ)是消息中间件中处理消费失败消息的重要机制。当消息消费失败次数超过配置的阈值(如 RocketMQ 默认 16 次重试),Broker 会将该消息转入死信 Topic。运维人员可以对死信队列中的消息进行人工排查、修复后重新投递或直接丢弃,防止问题消息无限重试阻塞消费流程。", + "source": null, + "related": [] + }, + { + "id": "fb-005", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "kafka", + "transactional-message" + ], + "question": "Kafka 事务机制中,生产者通过______ API 开启事务,所有跨分区的消息写入在该事务内是原子性的,即要么全部提交,要么全部回滚。", + "answer": [ + "initTransactions / beginTransaction" + ], + "answer_rule": "any", + "explanation": "Kafka 事务支持通过 Producer 的 initTransactions() 方法初始化事务协调器,然后在 sendMessages 过程中调用 beginTransaction() 开启事务,最后通过 commitTransaction() 或 abortTransaction() 提交或回滚。事务消息通过 Transaction Coordinator 和 __transaction_state 内部 Topic 来管理事务状态,保证跨分区的原子性写入,实现 Exactly-Once 语义。", + "source": null, + "related": [] + }, + { + "id": "fb-006", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "kafka", + "cluster" + ], + "question": "Kafka 集群中,每个 Partition 有一个 Leader 和多个______副本。只有 Leader 处理读写请求,Follower 仅负责同步数据。当 Leader 宕机时,Controller 从满足条件的 Follower 中选举新 Leader。", + "answer": [ + "Follower", + "follower" + ], + "answer_rule": "any", + "explanation": "Kafka 采用 Leader/Follower 主从架构。ISR(In-Sync Replicas)是与 Leader 保持同步的副本集合。Controller(集群中的一个 Broker)负责管理 Partition 的 Leader 选举。当 Leader 宕机,Controller 优先从 ISR 中选择第一个存活的 Follower 作为新 Leader,保证数据一致性。若 ISR 全部不可用,根据 unclean.leader.election.enable 配置决定是否允许不同步的副本接管。", + "source": null, + "related": [] + }, + { + "id": "fb-007", + "type": "fill_blank", + "difficulty": 3, + "tags": [ + "push-pull", + "consumer-group" + ], + "question": "Kafka 消费者采用______模式获取消息,消费者主动向 Broker 拉取数据。当某个消费者组中某个消费者宕机时,Broker 会触发______,将该消费者的 Partition 分配给组内其他消费者。", + "answer": [ + "pull", + "rebalance" + ], + "answer_rule": "all", + "explanation": "Kafka 采用 Pull(拉)模式,消费者根据自身处理能力主动从 Broker 拉取消息,相比 Push 模式有更好的流量控制能力。当 Consumer Group 中有消费者加入或离开时,会触发 Rebalance(重平衡),重新分配 Partition 的消费归属。Rebalance 期间消费暂停,因此需合理设置 session.timeout.ms 和 heartbeat.interval.ms 来平衡故障检测速度与误判风险。", + "source": null, + "related": [] + }, + { + "id": "fb-008", + "type": "fill_blank", + "difficulty": 4, + "tags": [ + "rocketmq", + "kafka", + "topic" + ], + "question": "RocketMQ 相比 Kafka,原生支持______机制,允许生产者在发送消息时附加 Tag,消费者可通过 Tag 过滤订阅感兴趣的消息,减少不必要的网络传输。", + "answer": [ + "Tag", + "tag", + "消息过滤" + ], + "answer_rule": "any", + "explanation": "RocketMQ 在 Topic 之下引入了 Tag 概念,生产者发送消息时可设置 Tag(如 OrderCreate、OrderCancel),消费者订阅时通过 Tag 表达式(如 * 或 || 或 &&)过滤消息。Broker 端支持基于 Tag 的服务端过滤,减少消费者拉取无关消息。Kafka 则依赖消息 Key 或消息体内容在客户端侧过滤,原生不支持服务端 Tag 过滤。", + "source": null, + "related": [] + }, + { + "id": "fb-009", + "type": "fill_blank", + "difficulty": 4, + "tags": [ + "kafka", + "durable" + ], + "question": "Kafka 的高性能写入依赖操作系统的______作为磁盘和网络之间的缓冲区。写入时数据先写入该缓存区,再由后台线程异步刷盘。若配置 acks=all 且______,则消息写入不会丢失。", + "answer": [ + "PageCache", + "min.insync.replicas >= 2" + ], + "answer_rule": "all", + "explanation": "Kafka 利用 PageCache(页缓存)实现高效的写入:生产者的消息先写入内核 PageCache,由 OS 后台线程异步刷盘(flush),避免同步 I/O 阻塞。配合 acks=all(所有 ISR 副本确认)和 min.insync.replicas >= 2 的配置,即使单个 Broker 宕机,只要 ISR 中还有其他副本持有数据,消息就不会丢失。这种设计在吞吐量和可靠性之间取得了平衡。", + "source": null, + "related": [] + }, + { + "id": "fb-010", + "type": "fill_blank", + "difficulty": 5, + "tags": [ + "kafka", + "pulsar", + "transactional-message" + ], + "question": "Kafka 的 Exactly-Once 语义通过幂等生产者(Producer)和______机制结合实现:幂等 Producer 为每条消息分配序列号,Broker 据此去重;事务机制则保证跨 Partition 写入的原子性。在消费端,需配合手动提交______和幂等消费逻辑才能端到端保证不丢不重。", + "answer": [ + "transaction", + "offset" + ], + "answer_rule": "all", + "explanation": "Kafka Exactly-Once 语义(EOS)由三层保障实现:(1) 幂等 Producer(enable.idempotence=true)通过 Producer ID + Sequence Number 在 Broker 端去重单分区内重复消息;(2) 事务机制(Transactional API)保证跨分区原子写入;(3) 消费端需配合 consumer.commitSync() 手动提交 offset,确保消费与提交在同一事务中完成(配合 transactional.id),再结合业务幂等(如数据库唯一键)实现端到端 Exactly-Once。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/interview-prep/message-queue/meta.json b/topics/interview-prep/message-queue/meta.json new file mode 100644 index 0000000..9705f8d --- /dev/null +++ b/topics/interview-prep/message-queue/meta.json @@ -0,0 +1,25 @@ +{ + "slug": "message-queue", + "name": "消息队列", + "description": "主流中间件选型、路由、持久化原理、事务、死信队列、推拉模式、集群化部署", + "tags": [], + "question_files": { + "single_choice": "single_choice.json", + "true_false": "true_false.json", + "fill_blank": "fill_blank.json", + "short_answer": "short_answer.json", + "code_reading": "code_reading.json" + }, + "stats": { + "total": 45, + "by_type": { + "single_choice": 15, + "true_false": 10, + "fill_blank": 10, + "short_answer": 5, + "code_reading": 5 + } + }, + "created": "2026-09-09", + "updated": "2026-09-09" +} \ No newline at end of file diff --git a/topics/interview-prep/message-queue/short_answer.json b/topics/interview-prep/message-queue/short_answer.json new file mode 100644 index 0000000..7045658 --- /dev/null +++ b/topics/interview-prep/message-queue/short_answer.json @@ -0,0 +1,148 @@ +{ + "topic": "message-queue", + "type": "short_answer", + "schema_version": "1.0.0", + "generated": "2026-09-09T15:30:00+08:00", + "questions": [ + { + "id": "sa-001", + "type": "short_answer", + "difficulty": 2, + "tags": [ + "kafka", + "cluster", + "durable" + ], + "question": "简述 Kafka 中 ISR(In-Sync Replicas)机制的工作原理,以及它如何影响消息的可靠性保证。", + "answer": "ISR 是 Kafka 中与 Leader 保持同步的副本集合。当 Producer 发送消息时,通过 acks 参数控制可靠性级别:acks=0 不等待确认(可能丢消息);acks=1 等待 Leader 写入成功;acks=all/-1 等待所有 ISR 副本都写入成功后才返回确认。Follower 副本通过从 Leader 拉取数据保持同步,当 Follower 落后超过 replica.lag.time.max.ms 配置的时间时会被移出 ISR。Leader 选举时只会从 ISR 中选择新 Leader,从而保证已确认的消息不会丢失。ISR 机制在可靠性和可用性之间提供了灵活的平衡:ISR 越小,可用性越高但可靠性越低;ISR 越大,可靠性越高但写入延迟可能增加。此外,unclean.leader.election.enable 参数控制是否允许非 ISR 副本参与选举,进一步影响一致性与可用性的取舍。", + "keywords": [ + "ISR", + "Leader", + "Follower", + "同步副本", + "acks", + "选举", + "可靠性和可用性权衡" + ], + "scoring_rubric": "提到 ISR 定义(与 Leader 同步的副本集合)得 1 分;说明 acks 参数的三种级别及其影响得 1 分;解释 Follower 同步机制与 ISR 成员变化得 1 分;说明 ISR 与 Leader 选举的关系得 1 分;提及可靠性和可用性的权衡得 1 分。满分 5 分。", + "explanation": "ISR 是 Kafka 分布式消息系统中最核心的可靠性机制之一。理解 ISR 需要掌握:(1) Kafka 采用 Leader-Follower 副本模型,只有 Leader 处理读写;(2) Follower 通过拉取方式同步 Leader 数据;(3) 只有在 ISR 中的副本才有资格被选为新 Leader;(4) Producer 的 acks 配置决定了消息写入需要多少副本确认。这三个方面共同构成了 Kafka 的消息可靠性保证体系。面试中需要能清晰阐述各配置项的含义以及它们如何相互配合。", + "source": null, + "related": [] + }, + { + "id": "sa-002", + "type": "short_answer", + "difficulty": 5, + "tags": [ + "rocketmq", + "transactional-message", + "exactly-once" + ], + "question": "请详细解释 RocketMQ 事务消息的实现机制(半消息机制),并对比 Kafka 的事务消息方案,分析两者在实现 Exactly-Once 语义上的差异。", + "answer": "RocketMQ 事务消息采用半消息(Half Message)机制,核心流程分为四步:(1) Producer 先发送半消息到 Broker,消息对消费者不可见(处于 PREPARE 状态);(2) Producer 执行本地事务;(3) 根据本地事务执行结果,向 Broker 发送 COMMIT 或 ROLLBACK 命令;(4) 若 Broker 长时间未收到确认,会主动回查 Producer 的本地事务状态。事务消息存储在 Topic %RETRY% 下,通过回查机制(最多 15 次)保证最终一致性。Kafka 事务消息则基于 Transaction Coordinator 和 PID(Producer ID)机制:Producer 向 Coordinator 注册并获取 PID 和 Epoch,通过两阶段提交(AddPartitionsToTxn + EndTxn)实现事务。Kafka 事务是跨分区的,可以原子性地写入多个分区;RocketMQ 事务消息是单条消息级别的原子性。Exactly-Once 语义方面:RocketMQ 依赖事务消息+消费端幂等(MessageKey 去重)来近似实现;Kafka 通过幂等 Producer(PID+序列号去重)+ 事务消息的组合实现端到端 Exactly-Once,但消费者需要配合 read_committed 隔离级别。", + "keywords": [ + "半消息", + "PREPARE", + "COMMIT", + "ROLLBACK", + "回查", + "Transaction Coordinator", + "PID", + "Exactly-Once", + "两阶段提交", + "幂等" + ], + "scoring_rubric": "准确描述 RocketMQ 半消息的四步流程得 2 分;说明回查机制得 1 分;描述 Kafka 事务的 Coordinator+PID 机制得 2 分;对比两者在事务粒度上的差异(单条 vs 跨分区)得 1 分;分析 Exactly-Once 实现路径的差异得 1 分。满分 7 分。", + "explanation": "事务消息是消息队列中的高级话题,面试中常以对比形式考察。RocketMQ 的半消息机制是其独创设计,核心在于消息先投递但对消费者不可见,通过二次确认和回查机制保证事务的最终一致性。Kafka 的事务模型更偏向流处理场景,支持跨分区原子写入,但不提供消息回查机制。理解两者的差异需要掌握分布式事务的基本理论(两阶段提交、最终一致性),以及各自中间件的内部实现细节。这个题目难度较高,考察对底层机制的深入理解。", + "source": null, + "related": [] + }, + { + "id": "sa-003", + "type": "short_answer", + "difficulty": 3, + "tags": [ + "dead-letter-queue", + "retry", + "message-trace" + ], + "question": "什么是死信队列(Dead Letter Queue)?请说明死信产生的常见原因、典型的重试与处理策略,以及如何通过死信队列实现消息回溯。", + "answer": "死信队列(DLQ)是专门存放无法被正常消费的消息的特殊队列。死信产生的常见原因包括:(1) 消息格式非法或反序列化失败;(2) 消费逻辑抛出异常且达到最大重试次数;(3) 消息 TTL 过期;(4) 队列/Topic 满了导致消息被拒绝;(5) 消费者显式拒绝消息(如 RabbitMQ 的 basic.reject 或 basic.nack 且 requeue=false)。典型处理策略:(1) 退避重试:指数退避 + 最大重试次数,避免雪崩;(2) 死信队列消费:专门的消费者进程监听死信队列,进行人工审核或告警;(3) 消息修复与重放:修复后将消息重新投递到原队列;(4) 消息回溯:RocketMQ 支持按时间戳回溯消费(reconsume),Kafka 支持通过 --offset 重置消费者位点来回溯历史消息。消息追踪方面,可以通过在消息中嵌入 TraceID,配合链路追踪系统(如 SkyWalking、Jaeger)实现端到端的消息追踪和死信消息的全链路定位。", + "keywords": [ + "死信队列", + "DLQ", + "重试", + "TTL", + "指数退避", + "回溯", + "TraceID", + "链路追踪" + ], + "scoring_rubric": "准确定义死信队列得 1 分;列举至少三种死信产生原因得 1 分;说明退避重试策略得 1 分;描述死信队列的消费和处理方式得 1 分;说明消息回溯的具体方法得 1 分。满分 5 分。", + "explanation": "死信队列是消息系统可靠性设计的重要环节。生产环境中,消息消费失败是不可避免的,关键是如何优雅地处理这些失败消息。面试者需要理解死信队列不仅是'存放失败消息的地方',更是一套完整的异常消息处理体系,包括重试策略、告警机制、人工介入流程和消息回溯能力。理解 RabbitMQ、RocketMQ、Kafka 各自在死信处理上的差异也很重要。", + "source": null, + "related": [] + }, + { + "id": "sa-004", + "type": "short_answer", + "difficulty": 3, + "tags": [ + "push-pull", + "consumer-group", + "rebalance" + ], + "question": "对比消息队列中 Push 和 Pull 两种消费模式的优缺点,并说明 Long Polling 如何结合两者的优势。在消费者端流控和 Rebalance 方面,这两种模式分别面临哪些挑战?", + "answer": "Push 模式(Broker 主动推送给消费者):优点是实时性高、延迟低;缺点是 Broker 无法感知消费者处理能力,可能导致消费者过载(消息堆积)、背压控制困难。Pull 模式(消费者主动拉取消息):优点是消费者自主控制消费速率,天然支持流控和背压;缺点是存在轮询开销,实时性依赖拉取间隔。Long Polling(长轮询)结合两者优势:消费者发起拉取请求后,Broker 不立即返回空结果,而是等待有新消息或超时后才响应,既减少了无效轮询开销,又保证了实时性,同时保留了消费者端的流控能力。RabbitMQ 原生使用 Push 模式(basic.deliver),通过 prefetch 机制做流控;Kafka 和 RocketMQ 使用 Pull 模式,Kafka 的 high-level consumer 使用 long polling(fetch.min.bytes 配置等待时间)。Rebalance 方面:Push 模式下 Broker 需要维护每个消费者的状态,rebalance 时需要通知所有消费者;Pull 模式下消费者通过 consumer group 协议自行协调分区分配,rebalance 时会触发位点迁移,可能导致短暂的重复消费或消息延迟。Kafka 的 Rebalance 有 Eager(全停全分)和 Cooperative(增量分配)两种策略,后者可以减少 stop-the-world 影响。", + "keywords": [ + "Push", + "Pull", + "Long Polling", + "prefetch", + "背压", + "流控", + "Rebalance", + "Eager", + "Cooperative", + "fetch.min.bytes" + ], + "scoring_rubric": "准确对比 Push 和 Pull 的优缺点各得 1 分;说明 Long Polling 的结合优势得 1 分;讨论流控/背压挑战得 1 分;分析 Rebalance 相关挑战得 1 分。满分 5 分。", + "explanation": "推拉模式是消息队列的基础架构设计问题。面试中需要能清晰地对比两种模式的本质差异,理解 Long Polling 的工程价值,以及在生产环境中 Rebalance 带来的实际挑战(如消费暂停、重复消费、位点丢失等)。Kafka 的 Cooperative Rebalance 是近年的重要改进,了解这个演进能体现对技术发展的关注。", + "source": null, + "related": [] + }, + { + "id": "sa-005", + "type": "short_answer", + "difficulty": 4, + "tags": [ + "kafka", + "rocketmq", + "pulsar", + "topic", + "partition", + "cluster" + ], + "question": "从消息模型、持久化架构、运维复杂度、适用场景四个维度,对比 Kafka、RocketMQ 和 Apache Pulsar 三款消息中间件的异同,并给出各自的最佳适用场景。", + "answer": "消息模型:Kafka 采用 Topic-Partition 模型,消费者通过 offset 顺序消费,天然适合流式处理;RocketMQ 采用 Topic-Queue 模型,支持 Tag 和 Key 进行消息过滤和路由,更适合业务消息场景;Pulsar 采用 Topic-Partition + Segment 存储分离模型,支持多租户和租户级别的资源隔离,同时兼容 Kafka 的消费语义。持久化架构:Kafka 使用 Broker 本地磁盘顺序写 + PageCache + 零拷贝(sendfile),写入性能极高但存储与计算耦合;RocketMQ 类似,使用 CommitLog + ConsumeQueue 的双层存储结构,CommitLog 顺序写所有消息,ConsumeQueue 存储索引;Pulsar 采用 BookKeeper 存储层,实现了计算存储分离,写入 BookKeeper 后异步落盘,支持分层存储(Tiered Storage)。运维复杂度:Kafka 中等,依赖 ZooKeeper(新版本移除),分区再均衡需关注;RocketMQ 较简单,NameServer 无状态易部署,运维工具完善;Pulsar 较高,需要同时运维 Broker + BookKeeper + ZooKeeper 三个组件。适用场景:Kafka 适合大数据流处理、日志采集、实时数仓(高吞吐场景);RocketMQ 适合电商交易、金融核心链路、事务消息场景(可靠性和消息过滤需求);Pulsar 适合多租户 SaaS 平台、跨地域多活、需要存储计算分离的大规模消息平台。", + "keywords": [ + "Topic", + "Partition", + "Queue", + "Tag", + "CommitLog", + "ConsumeQueue", + "BookKeeper", + "计算存储分离", + "零拷贝", + "多租户", + "Tiered Storage", + "ZooKeeper" + ], + "scoring_rubric": "消息模型维度对比准确得 1 分;持久化架构维度对比准确得 1 分;运维复杂度维度对比准确得 1 分;适用场景划分合理得 1 分;总体表述清晰、有深度得 1 分。满分 5 分。", + "explanation": "三大消息中间件的对比是面试高频题。关键不在于死记参数,而在于理解设计哲学的差异:Kafka 以高吞吐和流处理为核心,追求极致的顺序写和零拷贝性能;RocketMQ 脱胎于电商场景,强调消息的可靠投递和灵活路由;Pulsar 则是新一代架构,通过计算存储分离解决 Kafka 的存储扩展性问题。面试者需要展现出对架构演进的理解,能够根据具体业务场景给出合理的选型建议。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/interview-prep/message-queue/single_choice.json b/topics/interview-prep/message-queue/single_choice.json new file mode 100644 index 0000000..d836d87 --- /dev/null +++ b/topics/interview-prep/message-queue/single_choice.json @@ -0,0 +1,308 @@ +{ + "topic": "message-queue", + "type": "single_choice", + "schema_version": "1.0.0", + "generated": "2026-09-09T10:30:00+08:00", + "questions": [ + { + "id": "sc-001", + "type": "single_choice", + "difficulty": 1, + "tags": [ + "kafka", + "partition" + ], + "question": "在 Kafka 中,一个 Topic 被分为多个 Partition,以下关于 Partition 的描述正确的是?", + "options": { + "A": "Partition 内的消息是全局有序的", + "B": "每个 Partition 内的消息是有序的,但不同 Partition 之间不保证顺序", + "C": "Partition 的数量在创建 Topic 时固定后不可更改", + "D": "每条消息会被广播到所有 Partition" + }, + "answer": "B", + "explanation": "Kafka 的 Partition 是消息的物理分片单元,每个 Partition 内部的消息按照写入顺序严格有序(通过 offset 标识),但不同 Partition 之间不保证全局顺序。A 错误:全局有序需要只有一个 Partition。C 错误:Kafka 从 1.0 版本起支持动态增减 Partition 数量(但不能减少到低于当前最大 offset)。D 错误:消息通过分区器(Partitioner)根据 key 的 hash 值路由到特定的单个 Partition,而非广播。", + "source": null, + "related": [] + }, + { + "id": "sc-002", + "type": "single_choice", + "difficulty": 1, + "tags": [ + "dead-letter-queue" + ], + "question": "关于消息队列中的死信队列(Dead Letter Queue),以下说法正确的是?", + "options": { + "A": "死信队列中的消息会自动删除,无法再次消费", + "B": "死信队列用于存放消费失败且重试次数耗尽的消息", + "C": "死信队列只能由消息中间件自动创建,不支持手动配置", + "D": "死信队列中的消息优先级一定低于正常队列" + }, + "answer": "B", + "explanation": "死信队列(DLQ)的核心作用是存放因消费失败(如格式错误、业务异常、消费超时等)且重试次数耗尽而无法正常消费的消息,便于后续人工排查或修复后重新投递。A 错误:DLQ 中的消息默认持久化保存,消费者可以再次消费或由运维人员处理。C 错误:大多数消息中间件(如 RocketMQ、RabbitMQ)都支持手动配置死信队列的名称和策略。D 错误:死信队列没有自动降低优先级的说法,它的优先级取决于消费者的消费顺序。", + "source": null, + "related": [] + }, + { + "id": "sc-003", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "kafka", + "rocketmq", + "pulsar" + ], + "question": "以下关于 Kafka、RocketMQ 和 Pulsar 的对比,描述正确的是?", + "options": { + "A": "三者都采用 Broker + 存储耦合的架构设计", + "B": "Pulsar 采用计算与存储分离的架构,支持多租户和分层存储", + "C": "RocketMQ 的吞吐量一定高于 Kafka", + "D": "Kafka 原生支持事务消息,而 RocketMQ 不支持" + }, + "answer": "B", + "explanation": "Pulsar 采用 BookKeeper 作为存储层、Broker 作为计算层的分离架构,天然支持多租户命名空间和分层存储(将冷数据卸载到 S3 等对象存储)。A 错误:Pulsar 是计算存储分离的,Kafka 和 RocketMQ 是存储耦合的。C 错误:Kafka 在高吞吐场景(日志收集、大数据流处理)中吞吐量通常优于 RocketMQ;RocketMQ 的优势在于低延迟和丰富的消息特性(如事务消息、延迟消息)。D 错误:Kafka 从 0.11 版本开始支持事务(Exactly-Once 语义),RocketMQ 也原生支持事务消息(半消息机制),两者都支持。", + "source": null, + "related": [] + }, + { + "id": "sc-004", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "consumer-group" + ], + "question": "关于 Consumer Group 的行为,以下说法正确的是?", + "options": { + "A": "同一个 Consumer Group 内的不同消费者可以消费同一个 Partition 的消息", + "B": "不同 Consumer Group 之间消费进度互相影响", + "C": "同一个 Consumer Group 内,一条消息只会被其中一个消费者消费", + "D": "Consumer Group 的消费者数量必须等于 Partition 数量" + }, + "answer": "C", + "explanation": "Consumer Group 是消息队列实现「负载均衡「和「广播「的核心机制。同一个 Group 内,一条消息只会被组内的一个消费者消费(点对点模式),实现负载均衡。A 错误:同一个 Group 内,一个 Partition 在同一时刻只能被一个消费者消费。B 错误:不同 Group 之间完全独立,各自维护独立的消费位点(offset),互不影响。D 错误:消费者数量可以少于或多于 Partition 数量;少于时一个消费者消费多个 Partition,多于时部分消费者空闲。", + "source": null, + "related": [] + }, + { + "id": "sc-005", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "push-pull" + ], + "question": "关于消息队列的推(Push)拉(Pull)模式,以下描述正确的是?", + "options": { + "A": "Push 模式下消费者主动向 Broker 请求新消息", + "B": "Pull 模式下 Broker 主动将消息推送给消费者", + "C": "RocketMQ 采用 Push 模式时,底层实际上是基于长轮询(Long Polling)的 Pull 实现", + "D": "Kafka 的 Consumer 完全基于 Push 模式消费消息" + }, + "answer": "C", + "explanation": "RocketMQ 的 Consumer 虽然名为 PushConsumer(API 表现为推送语义),但底层实现是基于长轮询(Long Polling)的 Pull 模式:消费者定期向 Broker 发起拉取请求,如果当前没有新消息,Broker 会 hold 住该请求(挂起一段时间),直到有新消息到达或超时后才返回。这种设计结合了 Pull 模式的消费者端流控能力和 Push 模式的消息及时性。A 错误:Push 模式是 Broker 主动推送。B 错误:Pull 模式是消费者主动拉取。D 错误:Kafka 的 Consumer 完全基于 Pull 模式,由消费者控制拉取频率。", + "source": null, + "related": [] + }, + { + "id": "sc-006", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "kafka", + "partition" + ], + "question": "在 Kafka 中,以下哪种方式可以保证消息的全局顺序性?", + "options": { + "A": "将 Topic 的 Partition 数量设置为 3,并为消息设置 key", + "B": "将 Topic 的 Partition 数量设置为 1,所有消息不设置 key", + "C": "使用多个 Partition,通过消息 key 保证相同 key 的消息有序", + "D": "配置 acks=all 并设置 min.insync.replicas=2" + }, + "answer": "B", + "explanation": "Kafka 只能保证单个 Partition 内的顺序性。要实现全局有序,必须将 Partition 数量设为 1,使所有消息写入同一个 Partition,从而利用 Partition 内的 offset 顺序保证全局有序。A 错误:3 个 Partition 只能保证每个 Partition 内有序,Partition 之间不保证顺序。C 错误:相同 key 的消息会被路由到同一个 Partition,保证了 key 级别的局部有序,但不同 key 的消息仍然跨 Partition 分布,不保证全局有序。D 错误:acks 和 min.insync.replicas 控制的是数据可靠性和一致性,与消息顺序性无关。", + "source": null, + "related": [] + }, + { + "id": "sc-007", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "kafka", + "durable", + "topic" + ], + "question": "Kafka 的高吞吐量很大程度上依赖于其对零拷贝(Zero-Copy)技术的使用,关于该技术以下描述正确的是?", + "options": { + "A": "零拷贝通过内核态和用户态之间的内存映射实现数据传输", + "B": "零拷贝使用 sendfile 系统调用,使数据直接从磁盘(PageCache)传输到网卡,绕过用户空间", + "C": "零拷贝只能用于消费者首次消费消息的场景", + "D": "零拷贝要求消息必须存储在内存中而非磁盘上" + }, + "answer": "B", + "explanation": "Kafka 的 Consumer Fetch 数据时,使用 Linux 的 sendfile 系统调用实现零拷贝。传统 I/O 需要经过「磁盘→内核缓冲区→用户缓冲区→Socket 缓冲区→网卡」四次拷贝,而 sendfile 直接在内核态将数据从 PageCache 传输到网卡(DMA gather copy),绕过了用户空间,减少了上下文切换和内存拷贝的开销。A 错误:零拷贝的核心是 sendfile,不是 mmap。C 错误:零拷贝对所有消费读取都适用(包括重复消费和多消费者场景)。D 错误:零拷贝读取的数据来自 PageCache(磁盘数据在内核页缓存中的映射),不要求数据在应用层内存中。", + "source": null, + "related": [] + }, + { + "id": "sc-008", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "kafka", + "cluster" + ], + "question": "关于 Kafka 的 ISR(In-Sync Replicas)机制,以下描述正确的是?", + "options": { + "A": "ISR 是所有副本的集合,包括与 Leader 完全同步的和落后的副本", + "B": "ISR 中的副本必须与 Leader 保持数据同步,落后太多的副本会被移出 ISR", + "C": "当 ISR 中所有副本都确认写入后,Producer 才会收到 ack,这会导致可用性降低", + "D": "ISR 的大小永远等于 Replication Factor 的值" + }, + "answer": "B", + "explanation": "ISR 是与 Leader 保持数据同步的副本集合。Kafka 通过 replica.lag.time.max.ms 参数控制副本的最大允许落后时间,如果某个 Follower 副本落后超过该阈值,会被移出 ISR。这样保证了 ISR 中的副本都有能力在 Leader 故障时被选为新 Leader。A 错误:ISR 严格排除了落后过多的副本。C 错误:acks=all 只要求 ISR 中所有副本确认,ISR 缩小反而降低了写入延迟;可用性降低主要在 ISR 缩为 1 且该节点故障时发生。D 错误:ISR 大小是动态变化的,可能小于 Replication Factor(因副本落后被踢出),也可能等于(正常情况)。", + "source": null, + "related": [] + }, + { + "id": "sc-009", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "rocketmq", + "transactional-message" + ], + "question": "关于 RocketMQ 的事务消息机制,以下描述正确的是?", + "options": { + "A": "事务消息的半消息(Half Message)在发送后消费者立即可见", + "B": "事务消息通过两阶段提交实现:先发半消息,本地事务执行成功后发送 Commit,失败则发送 Rollback", + "C": "RocketMQ 事务消息依赖数据库 XA 事务来保证一致性", + "D": "事务消息的回查机制是由消费者端触发的" + }, + "answer": "B", + "explanation": "RocketMQ 事务消息采用两阶段提交:Producer 先发送半消息(Half Message)到 Broker,此时消费者不可见;然后 Producer 执行本地事务(如数据库操作);成功则发送 Commit 使消息对消费者可见,失败则发送 Rollback 删除消息。A 错误:半消息在 Commit 之前对消费者不可见,这是事务消息隔离性的关键。C 错误:RocketMQ 事务消息不依赖数据库 XA 事务,而是通过消息回查(Transaction Check)机制——如果 Broker 未收到 Commit 或 Rollback,会定期回查 Producer 本地事务状态来决定最终提交或回滚。D 错误:回查机制是由 Broker 端主动发起,查询 Producer 的本地事务状态。", + "source": null, + "related": [] + }, + { + "id": "sc-010", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "kafka", + "durable" + ], + "question": "Kafka 顺序写磁盘的性能优势来源于什么?", + "options": { + "A": "顺序写避免了磁盘寻道时间,同时利用了操作系统的 PageCache 做读写加速", + "B": "顺序写时磁盘的 IOPS 指标会显著提升", + "C": "顺序写意味着每条消息只写入一次,不需要追加写", + "D": "顺序写通过 RAID 0 条带化实现并行写入多个磁盘" + }, + "answer": "A", + "explanation": "Kafka 的核心设计哲学是将磁盘当作顺序写入的存储来使用。顺序写磁盘避开了随机 I/O 的磁盘寻道开销(机械硬盘的寻道时间通常 5-10ms),在顺序写入场景下磁盘吞吐量可达 600MB/s 以上。同时,Kafka 利用操作系统的 PageCache 作为读写缓冲层:写入时数据先写入 PageCache(用户态到内核态的一次拷贝),再由 OS 异步刷盘;读取时优先从 PageCache 返回,未命中时再读磁盘。B 错误:IOPS 是衡量随机 I/O 的指标,顺序写关注的是吞吐量(Throughput)。C 错误:Kafka 是 append-only 的追加写模式,每条消息确实只写一次,但这不是顺序写的性能来源。D 错误:RAID 条带化是硬件层面的并行方案,不是 Kafka 顺序写的设计原理。", + "source": null, + "related": [] + }, + { + "id": "sc-011", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "kafka", + "consumer-group", + "push-pull" + ], + "question": "当 Consumer Group 内有消费者加入或离开时,Kafka 会触发 Rebalance。关于 Rebalance,以下描述正确的是?", + "options": { + "A": "Rebalance 过程中消费者可以继续正常消费消息", + "B": "Rebalance 由 Consumer 端主动发起,Broker 不参与分配决策", + "C": "Rebalance 会导致所有消费者暂停消费,直到新的分区分配方案确定", + "D": "Rebalance 只在消费者宕机时才会触发" + }, + "answer": "C", + "explanation": "Kafka 的 Rebalance 是将 Partition 重新分配给 Consumer Group 内的消费者的过程。在 Rebalance 期间,所有消费者会暂停消费(进入 rebalancing 状态),直到分配方案确定后才恢复。A 错误:Rebalance 期间消费者无法消费消息,这是一个已知的延迟来源。B 错误:在 Kafka 2.3+ 的 Cooperative Rebalance(增量再平衡)中 Broker(Group Coordinator)参与协调,且新版 Kafka 支持 Sticky 分配策略,由协调者参与决策。D 错误:Rebalance 不仅在宕机时触发,消费者主动加入/离开(如发布新版本重启、扩容缩容)、心跳超时(session.timeout.ms)、Poll 超时(max.poll.interval.ms)等都会触发 Rebalance。", + "source": null, + "related": [] + }, + { + "id": "sc-012", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "kafka", + "transactional-message" + ], + "question": "关于 Kafka 的 Exactly-Once 语义实现,以下描述正确的是?", + "options": { + "A": "Kafka 通过acks=all就能实现端到端的Exactly-Once语义", + "B": "Kafka的Exactly-Once语义依赖于幂等Producer(PID+序列号去重)和事务API的组合", + "C": "Kafka的Exactly-Once语义仅适用于Producer端,Consumer端无法实现", + "D": "Kafka的Exactly-Once语义要求所有Partition的Replication Factor必须为1" + }, + "answer": "B", + "explanation": "Kafka 的 Exactly-Once 语义由两个机制组合实现:(1) 幂等 Producer——Broker 为每个 Producer 分配唯一的 PID(Producer ID),每条消息携带递增的序列号,Broker 端通过 PID+序列号去重,解决单个 Producer 到 Broker 的重复问题;(2) 事务 API——Producer 可以在一个原子事务中写入多条消息(跨 Partition),确保要么全部提交要么全部回滚,解决跨 Partition 的原子写入问题。A 错误:acks=all 只保证消息被所有 ISR 副本接收,不能解决 Producer 重试导致的重复发送问题,更不是端到端的 Exactly-Once。C 错误:Consumer 配合 read_committed 隔离级别和幂等消费逻辑(如数据库唯一键),也可以实现端到端的 Exactly-Once。D 错误:Replication Factor 与 Exactly-Once 无关。", + "source": null, + "related": [] + }, + { + "id": "sc-013", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "kafka", + "durable" + ], + "question": "以下关于 Kafka 中 PageCache 和 fsync 策略的描述,哪项是正确的?", + "options": { + "A": "Kafka 默认使用同步刷盘(每条消息写入后立即 fsync),以确保数据不丢失", + "B": "Kafka 依赖操作系统的 PageCache 做写缓冲,由操作系统异步刷盘,这在 broker 崩溃时可能导致少量数据丢失", + "C": "配置 log.flush.interval.messages=1 可以实现每条消息都 fsync,同时不影响写入吞吐量", + "D": "PageCache 是用户态的内存缓存,用于缓存 Kafka 的日志数据" + }, + "answer": "B", + "explanation": "Kafka 的默认设计是利用操作系统的 PageCache 作为写入缓冲:Producer 写入的数据先到达 PageCache(内核态),然后由操作系统的 pdflush 等机制异步刷盘。这意味着如果 Broker 进程崩溃(OS 仍正常运行),PageCache 中未刷盘的数据不会丢失;但如果 OS 也崩溃(断电),未刷盘的数据可能丢失。这是 Kafka 在吞吐量和可靠性之间的权衡。A 错误:Kafka 默认不是同步刷盘,否则吞吐量会大幅下降。C 错误:虽然配置 log.flush.interval.messages=1 可以每条消息 fsync,但这会严重降低写入吞吐量,是已知的性能反模式。D 错误:PageCache 是内核态的内存页缓存,不是用户态的。", + "source": null, + "related": [] + }, + { + "id": "sc-014", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "dead-letter-queue", + "rocketmq" + ], + "question": "在 RocketMQ 中,关于死信队列和消息重试的机制,以下描述正确的是?", + "options": { + "A": "消息消费失败后直接进入死信队列,不进行任何重试", + "B": "RocketMQ 的重试队列支持按时间延迟重试,重试次数耗尽后消息自动转入死信队列", + "C": "死信队列中的消息只能由运维人员手动删除,不支持再次消费", + "D": "RocketMQ 的重试次数由 Broker 全局统一配置,消费者无法自定义" + }, + "answer": "B", + "explanation": "RocketMQ 的消息重试机制:当消息消费失败时,Broker 会将消息投递到内置的重试队列(%RETRY%{ConsumerGroup})。重试队列按照延迟级别组织(默认 18 个级别:10s, 30s, 1m, 2m, 3m, ... 2h),消息会按照延迟级别逐步重试。当重试次数超过配置的最大重试次数(默认 16 次)后,消息会被自动转入死信队列(%DLQ%{ConsumerGroup})。A 错误:消费失败后会先进入重试队列进行延迟重试,而非直接进死信队列。C 错误:死信队列中的消息可以被消费者消费(通常是由专门的死信消费者来处理),也可以选择删除。D 错误:最大重试次数由消息的属性(reconsumeTimes)控制,消费者可以通过修改消息属性来自定义重试行为。", + "source": null, + "related": [] + }, + { + "id": "sc-015", + "type": "single_choice", + "difficulty": 5, + "tags": [ + "kafka", + "cluster" + ], + "question": "在 Kafka 集群中,当 Leader 副本所在的 Broker 宕机后,以下关于 Leader 选举的描述正确的是?", + "options": { + "A": "Kafka 使用 Paxos 算法进行 Leader 选举,保证强一致性", + "B": "ISR 中的存活副本会参与 Leader 选举,由 Controller 从 ISR 中按优先级选择新 Leader", + "C": "所有 Follower 副本都会参与投票,得票最多的成为新 Leader", + "D": "Leader 选举完成后,所有 Follower 需要从新的 Leader 全量同步数据才能开始提供读服务" + }, + "answer": "B", + "explanation": "Kafka 的 Leader 选举由集群中的 Controller(一个特殊的 Broker,通过 ZooKeeper/KRaft 选举产生)负责管理。当 Leader 副本故障时,Controller 从该 Partition 的 ISR 列表中选择一个存活的副本作为新 Leader(如果配置了 unclean.leader.election.enable=true,ISR 全部不可用时还可能从非 ISR 副本中选择,但会有数据丢失风险)。新 Leader 选定后立即对外提供服务,其他 Follower 在新 Leader 上开始拉取数据逐步同步。A 错误:Kafka 不使用 Paxos,早期使用 ZooKeeper 的 ZAB 协议,新版 Kafka 使用内置的 KRaft(基于 Raft 的变体)进行元数据管理。C 错误:Kafka 不采用投票机制,而是由 Controller 直接从 ISR 中选择。D 错误:新 Leader 选出后立即可用,Follower 异步追赶数据,不需要全量同步完成后才提供服务(这正是 ISR 机制的意义——ISR 中的副本数据已经足够接近 Leader)。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file diff --git a/topics/interview-prep/message-queue/true_false.json b/topics/interview-prep/message-queue/true_false.json new file mode 100644 index 0000000..38ea04d --- /dev/null +++ b/topics/interview-prep/message-queue/true_false.json @@ -0,0 +1,160 @@ +{ + "topic": "message-queue", + "type": "true_false", + "schema_version": "1.0.0", + "generated": "2026-09-09T15:30:00+08:00", + "questions": [ + { + "id": "tf-001", + "type": "true_false", + "difficulty": 3, + "tags": [ + "kafka", + "零拷贝", + "持久化" + ], + "question": "Kafka 的零拷贝(Zero-Copy)技术依赖于 Linux 操作系统的 sendfile 系统调用,Java 层面通过 FileChannel.transferTo() 方法触发,其核心优势是避免了数据在用户态和内核态之间的拷贝。", + "answer": true, + "explanation": "正确。Kafka 在将磁盘文件数据发送到网络 Socket 时,使用了操作系统的 sendfile 系统调用(通过 Java NIO 的 FileChannel.transferTo() 触发)。sendfile 直接在内核空间完成文件数据到网卡缓冲区的传输,无需将数据拷贝到用户态再写回内核态,显著减少了 CPU 开销和内存拷贝次数,是 Kafka 高吞吐量的关键优化之一。", + "source": null, + "related": [] + }, + { + "id": "tf-002", + "type": "true_false", + "difficulty": 2, + "tags": [ + "rocketmq", + "事务消息", + "半消息" + ], + "question": "RocketMQ 的事务消息机制依赖于半消息(Half Message)实现:Producer 先发送半消息到 Broker,执行本地事务后再根据结果 commit 或 rollback,Broker 对消费者隐藏未确认的半消息。", + "answer": true, + "explanation": "正确。这是 RocketMQ 事务消息的核心机制。Producer 发送半消息后,Broker 会将其存储在内部的 Half Topic 中,此时消费者无法消费该消息。Producer 执行本地事务后,根据结果向 Broker 发送 commit 或 rollback 指令。commit 后消息进入真正的 Topic 可被消费;rollback 后消息被丢弃。若 Broker 未收到确认,会定时回查 Producer 的本地事务状态。", + "source": null, + "related": [] + }, + { + "id": "tf-003", + "type": "true_false", + "difficulty": 4, + "tags": [ + "pulsar", + "架构", + "计算存储分离" + ], + "question": "Apache Pulsar 采用计算存储分离架构,Broker 节点是无状态的,消息数据持久化在 Apache BookKeeper 中;而 Kafka 的 Broker 节点同时承担计算和存储职责,消息数据直接存储在 Broker 本地磁盘上。", + "answer": true, + "explanation": "正确。这是 Pulsar 和 Kafka 架构的根本区别。Pulsar 的 Broker 不存储数据,仅负责协议处理和读写路由,实际数据写入 BookKeeper 的 Bookie 节点;而 Kafka 的 Broker 既处理客户端请求(计算),又将数据存储在本地磁盘的日志段中(存储)。这种分离使得 Pulsar 在 Broker 扩缩容时无需数据迁移,而 Kafka 扩容 Broker 后需要进行分区重分配(rebalance)。", + "source": null, + "related": [] + }, + { + "id": "tf-004", + "type": "true_false", + "difficulty": 2, + "tags": [ + "rocketmq", + "推拉模式", + "长轮询" + ], + "question": "RocketMQ 的 Consumer 采用 Push(推)模式消费消息,即 Broker 主动将消息推送给消费者;而 Kafka 的 Consumer 始终采用 Pull(拉)模式,由消费者主动向 Broker 拉取消息。", + "answer": false, + "explanation": "错误。虽然 RocketMQ 的 Consumer API 是 Push 模式(注册 MessageListener 回调),但底层实现实际上是基于长轮询(Long Polling)的 Pull 模式。Consumer 向 Broker 发起长轮询请求,Broker 在有新消息时立即响应,无消息时保持连接等待。Kafka 的 Consumer 确实是 Pull 模式。因此 RocketMQ 并非真正的 Broker 主动推送,而是通过长轮询模拟了 Push 的实时性。", + "source": null, + "related": [] + }, + { + "id": "tf-005", + "type": "true_false", + "difficulty": 3, + "tags": [ + "kafka", + "集群", + "ISR", + "选举" + ], + "question": "Kafka 的 ISR(In-Sync Replicas)机制仅用于控制 follower 副本从 leader 副本同步数据的时延阈值(replica.lag.time.max.ms),与数据一致性和容灾选举无关。", + "answer": false, + "explanation": "错误。ISR 是 Kafka 保障数据一致性和可用性的核心机制,绝不仅仅控制同步延迟。ISR 维护的是与 Leader 保持同步的副本集合:当 follower 副本落后超过 replica.lag.time.max.ms 时会被踢出 ISR。ISR 的作用包括:1)acks=all 时只有 ISR 中所有副本确认才算写入成功,保障数据一致性;2)Leader 宕机时,Controller 优先从 ISR 中选举新 Leader,保障可用性和数据完整性。ISR 是数据一致性和容灾选举的关键枢纽。", + "source": null, + "related": [] + }, + { + "id": "tf-006", + "type": "true_false", + "difficulty": 3, + "tags": [ + "kafka", + "事务消息", + "exactly-once" + ], + "question": "Kafka 的事务消息机制允许 Producer 在一个事务内跨多个分区原子性地写入消息,配合 Consumer 的 isolation.level=read_committed 配置,可实现端到端的 Exactly-Once 语义。", + "answer": true, + "explanation": "正确。Kafka 从 0.11 版本引入事务支持。Producer 通过 initTransactions()、beginTransaction()、commitTransaction() 等 API 在一个事务内跨分区写入消息,由 TxnCoordinator 协调事务状态。Consumer 设置 isolation.level=read_committed 后,只能读取已提交事务的消息。结合幂等 Producer(enable.idempotence=true),Kafka 可在 Producer→Broker→Consumer 链路上实现 Exactly-Once 语义。", + "source": null, + "related": [] + }, + { + "id": "tf-007", + "type": "true_false", + "difficulty": 2, + "tags": [ + "消息模型", + "广播", + "consumer-group" + ], + "question": "在 RocketMQ 中,广播消费(BROADCASTING)模式下,同一个 Consumer Group 中的每个 Consumer 实例都会收到全量消息的副本;而集群消费(CLUSTERING)模式下,同组内只有一个实例收到某条消息。", + "answer": true, + "explanation": "正确。RocketMQ 支持两种消费模式:广播消费(BROADCASTING)下,消息会投递给 Consumer Group 中的每一个实例,每个实例独立消费全量消息,适用于通知、配置更新等场景;集群消费(CLUSTERING)下,同组内的消息被负载均衡分配给其中一个实例处理,适用于任务分发场景。注意:广播消费模式下 rebalance 不生效,因为每个实例都需要消费所有消息。", + "source": null, + "related": [] + }, + { + "id": "tf-008", + "type": "true_false", + "difficulty": 4, + "tags": [ + "死信队列", + "重试策略", + "消息可靠性" + ], + "question": "在 RocketMQ 中,当消费端处理消息失败后,消息会自动进入重试队列进行多次重试;超过最大重试次数后,消息会被自动路由到死信队列(DLQ),无需开发者额外配置死信队列的消费逻辑即可完成消息回溯。", + "answer": false, + "explanation": "错误。前半句正确——RocketMQ 确实会在消费失败后将消息放入重试 Topic(%RETRY%)进行重试,达到最大重试次数后自动转入死信队列(%DLQ%)。但后半句错误:死信队列中的消息不会自动被消费或回溯。开发者必须编写专门的消费者订阅死信队列 Topic 进行处理(如告警、人工干预或写入落库)。如果不对死信队列进行消费,消息将永久滞留在 DLQ 中,造成消息丢失。", + "source": null, + "related": [] + }, + { + "id": "tf-009", + "type": "true_false", + "difficulty": 5, + "tags": [ + "kafka", + "消费者", + "offset", + "exactly-once" + ], + "question": "Kafka 消费者在处理完消息后立即提交 offset(即 process-then-commit 模式),配合自动提交机制,即可实现 Exactly-Once 消费语义,无需额外的幂等处理。", + "answer": false, + "explanation": "错误。这里存在两个陷阱:1)「自动提交」(enable.auto.commit=true)是在 poll() 调用时自动提交上次的 offset,而非在消息处理完成后提交,这意味着如果消费者在处理消息过程中崩溃,已提交的 offset 对应的消息实际上未被处理,导致消息丢失(At-Most-Once);2)即使手动在处理完成后提交 offset(At-Least-Once),也只是保证消息至少被处理一次,仍可能出现重复消费。要实现 Exactly-Once,需要结合幂等消费(如数据库唯一键去重)或使用 Kafka 事务将 offset 提交与消息处理绑定在同一事务中。process-then-commit + 自动提交不能实现 Exactly-Once。", + "source": null, + "related": [] + }, + { + "id": "tf-010", + "type": "true_false", + "difficulty": 2, + "tags": [ + "pulsar", + "消息模型", + "确认机制" + ], + "question": "Pulsar 同时支持累积确认(Cumulative Acknowledgment)和单条确认(Individual Acknowledgment)两种消费确认模式,而 Kafka Consumer 仅支持对 poll() 批次内已处理消息的批量提交,不支持选择性确认单条消息。", + "answer": false, + "explanation": "错误。后半句关于 Kafka 的描述不准确。Kafka 从 0.11 版本起支持通过 ConsumerRecord 记录每条消息的 offset,开发者可以使用 consumer.commitSync(Collection) 方法选择性地提交特定分区和 offset 的确认,实现类似「单条确认」的效果。此外,Kafka 还支持通过 ConsumerRebalanceListener 在 rebalance 时精确控制 offset 提交。Pulsar 确实原生支持累积确认和单条确认两种模式,但说 Kafka 不支持选择性确认是错误的。", + "source": null, + "related": [] + } + ] +} \ No newline at end of file