微服务最佳实践 本文记录了工作当中积累的一些关于微服务最佳实践的想法 一定要做到的点 每个请求一定要有 request-id,一般由调用方生成,如果你是在推动一个遗留项目,那么可以在网关或者AOP层去生成它,并且在日志会和调用链路中携带上这个ID,这样才能够在繁杂的日志中找到特定的请求信息。建议由 uuid 或者 雪花算法 生成。 请求日志和错误日志分离,这么是因为这两者具有完全不同的关注点,且前者格式相对固定。分离他们可以在清洗日志时使用不同的格式。 当请求发生错误时(无论是预期内的还是预期外)都应该体现在日志当中 如果你们的系统引入了调用链追踪,那么可以使用 request-id 作为 trace-id。 对于每一个资源的操作都应有反馈,比如删除不存在的资源或者创建同名的资源,我们都应该返回特定的错误码,通常可以使用三类: NotFoundError, ConflictError, InternalError,ValidationError,同时错误码应该有所区分——预期和非预期的(比如使用code的范围去区分是一个选择),以便于落地告警 网关和微服务打印日志建议分开侧重点,网关应该打印协议层的细节,比如 http header, http code, cookie等,而微服务应该打印 应用Code, 请求参数,响应参数。这样可以在网关统计到后端微服务有多少是因为转发时的网络问题而导致访问失败,多少又是因为应用服务内的问题而失败。 系统和服务都应该有明确的边界,服务边界很容易理解,就是一个独立的部署单元,系统的边界是指对所有系统外的服务的交互都应该收口,这样才能更好的进行流量治理。理想情况下我们可以通过网络策略来形成物理边界,比如k8s的namespace。但是在真实的情况中可能你要面对很多遗留服务,它们已经通过IP地址或者服务发现互相访问了,直接去改造它们成本非常大,那么你可以在逻辑上形成边界,比如提炼一个新的概念对象,将现存的服务都分门别类,然后再请求上就可以区分出流量到底来自外部还是内部。不过值得注意的是,物理边界虽然理想,但是在做异地多活时可能会面临一些挑战,比如你使用namespace来作为网络边界,那么你需要把不同地区和机房的机器都包含到一个集群中才能满足不同程度的容灾需求。 谨慎对待 Error 和 Warning 级别的日志,它们可以用于反馈当前服务的健康度,可以对其进行监控和告警,同时为了控制好整个程序中的错误级别,建议不要在代码中hardcode打印日志,统一返回到上层去区分处理。 乐观锁可以使用最后更新时间或者http标准下的ETag 请求时一定要注意超时时间,尤其是调用链路比较长时,要保证 client 和 server 的 context 声明周期一致,通常来说,client 不应该设置自己的超时时间,统一由服务端控制。但是如果基础设施不稳定的,话,这种方式可能会导致较长的无效等待,为了达到更高的可用性,全链路超时是必须的,超时不由服务端决定,而是客户端决定。 在没有使用 ThreadLocal 技术的项目中。无论在开发or基础库都应该注意日志打印时预留context参数,便于日志染色 枚举的转义关系应尽量交给客户端来渲染,客户端可以从枚举的维护方自行下载缓存到本地做映射,这里有几个考虑 暴露枚举值的转义表之后,各个接入方都可以自行转义,包括前端。如果你期望将转义关系隐藏在后端,只有当你的枚举值不被其他服务所关心时才能这样做,且没有带来太多的价值。 每个消费房对枚举转义形式可能期望不同,比如一个数组的表现形式,树当中一个节点的表现形式等,都可能不同 如果枚举的定义并不是你的服务,且业务逻辑并没有依赖这部分数据,这时仅为了满足某个消费方(通常来说是前端orApp)去依赖其他业务接口来完成转义,其实是引入了更多风险,并且没有必要,因为需要这个转义的消费方多数时候其实已经获得过转义表 对于预期外的行为,服务必须要有响应,比如: 获取、更新、删除一个不存在的记录 写入冲突 项目结构(project layout) 这真是一个非常常见的问题了,我觉得这个问题需要结合场景来看待: 编程语言:每个语言都有自己比较常见的项目结构,比如 golang,在各自的标准下的结构才是我们需要考量如何区分的。 架构风格:由于解决的问题不同,你的软件可能会采用不同的架构风格,比如 数据流式(批处理,管道),请求/响应式(N层,面向对象),独立组件式(交互进程,事件驱动),数据中心式(数据库,黑板系统)可以参考 Architectural-Styles-Patterns。使用不同架构风格时,你的项目结构也会调整。 总的来看项目结构很难以一个万金油的模版去一统天下,都需要结合各自语言的特性以及不同架构风格来解决,这里提供一些常见选择: