3. 第一个生产级函数应用

上一节我们理解了无服务器架构的基本形态,这一节把视角落到一个真正可交付的工程对象上:一个具备路由、请求验证、结构化日志和本地调试能力的 HTTP 触发函数应用。所谓“生产级”,并不是指它已经承载了真实流量,而是指它从第一天起就具备可维护、可观测、可复现的基本素质——路由清晰、输入受控、日志可检索、本地能跑通。这四点恰好对应了函数应用从“能跑”到“敢用”的分界线。

从模板到项目骨架

创建 HTTP 触发函数最直接的方式是使用 Azure Functions Core Tools。在本地终端执行 func init MyHttpApi --dotnet(或选择 --javascript、--python 等运行时),随后进入目录执行 func new --template "HttpTrigger" --name HealthCheck。Core Tools 会生成一个包含 host.json、local.settings.json 以及函数代码的项目结构。其中 local.settings.json 只用于本地调试,不应提交到版本库;host.json 则承载运行时级别的配置,例如日志采样和扩展捆绑版本。

默认模板生成的函数通常监听 /api/HealthCheck,并接受 GET 与 POST。但在生产级 API 中,我们往往希望路由具备语义,例如 /api/v1/orders/{orderId}。Azure Functions 允许在 HttpTrigger 特性中通过 Route 参数自定义路由模板,花括号表示路径参数,{orderId:guid} 这类约束还能在路由层直接完成格式校验,避免非法请求进入函数体。

路由与请求验证的分工

路由解决的是“请求该由谁处理”,请求验证解决的是“这个请求是否值得处理”。二者不应混在一起。路由层适合做路径结构、HTTP 方法和参数类型的约束;函数体内则负责业务语义校验,例如订单号是否存在、请求体字段是否完整、金额是否为正数。把校验前移到路由层可以减少无效调用,把业务校验留在函数体内则保证逻辑内聚。

一个常见的做法是定义一个请求模型,并在函数入口处显式校验。例如对于创建订单的 POST 请求,函数首先反序列化 JSON,然后检查 customerId 是否为空、items 是否至少包含一项、totalAmount 是否大于零。任何一项不满足,立即返回 400 Bad Request 并附带结构化错误信息,而不是让异常在后续代码中爆发。这样调用方能在第一时间知道问题所在,日志中也能留下明确的拒绝原因。

HTTP 触发函数应用的请求处理链 调用方 HTTP Client 路由层 /api/v1/orders/{orderId:guid} 方法约束 · 路径参数 函数体 反序列化请求体 业务语义校验 调用下游服务 结构化日志 TraceId · OrderId StatusCode · Duration 400 Bad Request 路由不匹配 / 参数非法 422 / 400 校验失败 字段缺失 · 金额非正 200 OK 返回资源标识 本地调试观察点 func start 启动后,终端输出函数监听地址与路由列表 使用 curl 或 REST Client 发送请求,观察状态码与响应体 日志中确认 TraceId 贯穿、耗时字段与业务字段同时出现

结构化日志:让日志可被查询

很多初学者把日志写成字符串拼接,例如 log.LogInformation("订单创建成功 " + orderId)。这种日志在本地看没问题,一旦进入 Application Insights 就难以按字段过滤。结构化日志的核心是把关键信息作为键值对输出,例如使用 ILogger 的占位符写法:log.LogInformation("Order {OrderId} created for customer {CustomerId}", orderId, customerId)。这样在 Azure Monitor 中就可以直接按 OrderId 或 CustomerId 查询,而不需要全文检索。

生产级日志还应包含请求级别的上下文:TraceId、HTTP 方法、路由、状态码、处理耗时。Azure Functions 的 HttpRequest 对象可以读取 traceparent 头,配合 Application Insights 的分布式追踪,能把一次调用在多个服务间的链路串起来。日志级别也要有纪律:Information 用于正常业务里程碑,Warning 用于可恢复的异常输入,Error 用于需要人工介入的失败。不要把所有信息都打成 Error,否则告警会失去意义。

本地调试的完整闭环

本地调试不是“随便跑一下”,而是要在没有云资源的情况下验证核心行为。执行 func start 后,Core Tools 会读取 local.settings.json 中的 AzureWebJobsStorage 等配置,并启动一个本地运行时。终端会列出所有函数的监听地址,例如 http://localhost:7071/api/v1/orders/{orderId}。此时可以用 curl 发送请求:

curl -X POST http://localhost:7071/api/v1/orders \
  -H "Content-Type: application/json" \
  -d '{"customerId":"C-1001","items":[{"sku":"A1","qty":2}],"totalAmount":39.8}'

观察终端输出,应该能看到结构化日志中同时出现 OrderId、CustomerId 和状态码。如果返回 400,检查请求体字段是否与模型匹配;如果返回 404,检查路由模板中的 {orderId:guid} 是否要求了 GUID 格式而实际传入了非 GUID 值。本地调试的价值在于:它把“路由—校验—日志”三条线在一个终端里同时暴露出来,让问题定位从猜测变成观察。

建议在 local.settings.json 中设置 "FUNCTIONS_WORKER_RUNTIME" 与所选语言一致,并保留 "IsEncrypted": false 仅用于本地。生产环境的连接字符串和密钥应通过应用设置或 Key Vault 注入,不要硬编码在代码或配置文件中。

一个可参考的请求验证清单

为了让“请求验证”不流于口号,可以把校验点整理成一份可执行的检查清单。以下清单以创建订单为例,字段和阈值均为示例,实际项目应按业务规则调整:

校验层检查项不通过时的响应
路由层HTTP 方法是否为 POST405 Method Not Allowed
路由层路径参数 orderId 是否符合 GUID 格式404 Not Found
函数体请求体是否为合法 JSON400 Bad Request
函数体customerId 非空且长度不超过 64400 Bad Request
函数体items 至少包含一项,qty 为正整数400 Bad Request
函数体totalAmount 大于 0 且不超过 100000400 Bad Request

这份清单的意义在于:它把“验证”从模糊的代码直觉变成了可逐项核对的契约。每一条都对应一个明确的输入条件和输出状态码,测试时可以直接按行构造用例。当团队多人协作时,这份清单也是接口文档的一部分。

从本地到生产的配置差异

本地调试通过后,下一步是把函数应用部署到 Azure。此时需要关注几个关键差异:本地使用 localhost:7071,生产使用 https://<app-name>.azurewebsites.net;本地日志输出到终端,生产日志进入 Application Insights;本地存储使用 Azurite 模拟,生产使用真实 Storage Account。这些差异不应改变函数代码本身,而应通过配置注入来隔离。例如日志采样率、CORS 允许来源、超时时间都可以在 host.json 或应用设置中调整,代码只依赖抽象接口。