ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

禁止 Feign!我们为什么自研 InternalServiceClient

禁止 Feign!我们为什么自研 InternalServiceClient 文章目录禁止 Feign我们为什么自研 InternalServiceClient一、微服务跨服务调用为什么不用 Feign二、Feign 的痛点2.1 上下文传递困难2.2 超时控制不灵活2.3 降级处理复杂2.4 接口定义和实现割裂三、InternalServiceClient 的设计3.1 核心设计理念3.2 上下文自动传递3.3 超时逐层递减3.4 基于 Nacos 服务发现3.5 响应类型安全3.6 切面链增强四、代码对比4.1 Feign 方式4.2 InternalServiceClient 方式五、总结设计哲学禁止 Feign我们为什么自研 InternalServiceClient微服务跨服务调用90% 的人用 Feign——但我们选择不。一、微服务跨服务调用为什么不用 Feign国内做 Spring Cloud 微服务跨服务调用几乎只有一个答案Feign。OpenFeign 确实是经典方案声明式接口、自动序列化、集成 Ribbon 负载均衡。但用久了就会发现它解决了一些问题的同时引入了更多新问题。在 MetaLite 的设计过程中我们做了一个决定禁止使用 Feign自研 InternalServiceClient。这个决定在团队内部也争论了很久。毕竟 Feign 是 Spring Cloud 的标配不用它意味着要自己写一套调用框架。但经过几个项目的踩坑我们确信这是一个正确的选择。这篇文章我把 Feign 的痛点、InternalServiceClient 的设计思路、以及两者的代码对比说清楚。看完之后你可以自己判断。二、Feign 的痛点2.1 上下文传递困难微服务调用链中有很多上下文信息需要在服务间传递TraceId全链路追踪排查问题刚需用户 ID下游服务需要知道是谁发起的请求AppId调用方标识用于权限控制和审计Seata XID分布式事务的 transaction ID用 Feign 时这些信息需要通过RequestInterceptor手动注入到 HTTP Header 中ComponentpublicclassFeignHeaderInterceptorimplementsRequestInterceptor{Overridepublicvoidapply(RequestTemplatetemplate){template.header(traceId,ThreadContext.getTraceId());template.header(userId,ThreadContext.getLoginUserId());template.header(appId,ThreadContext.getAppId());template.header(X-Seata-XID,ThreadContext.getSeataXid());}}看似没问题但有两个隐患隐式依赖Feign 的 Header 传递是全局拦截器做的某个服务忘了配拦截器上下文就断了排查困难传递逻辑分散TraceId、用户 ID、XID 各自在不同的拦截器或拦截逻辑中处理缺少统一入口2.2 超时控制不灵活Feign 的超时配置粒度很粗。通常是在application.yml中全局设置feign:client:config:default:connectTimeout:5000readTimeout:10000问题是不同调用的超时需求差异很大。查询用户基本信息50ms 就够了生成报表导出可能需要 30 秒批量同步数据可能要 1 分钟用 Feign 要么全局改影响其他调用要么每个 Client 单独配配置类代码膨胀。更严重的是在微服务调用链中如果 A → B → C 每层都用 10 秒超时最终用户可能要等 30 秒才拿到响应。这就是超时累积效应——调用链越长最终响应越慢。2.3 降级处理复杂Feign 的降级fallback依赖 Hystrix 或 Resilience4jFeignClient(nameuser-service,fallbackUserClientFallback.class)publicinterfaceUserClient{GetMapping(/api/user/info)RespUserDtogetUserInfo(RequestParamStringuserId);}ComponentpublicclassUserClientFallbackimplementsUserClient{OverridepublicRespUserDtogetUserInfo(StringuserId){returnResp.error(用户服务暂时不可用);}}每个接口都要写一个 Fallback 实现类。服务越多Fallback 类越多代码量直线上升。而且Fallback 是接口级别的不是调用级别的。同一个接口有的调用方需要降级有的不需要但 Feign 只能配一个全局策略。2.4 接口定义和实现割裂Feign 要求调用方定义接口FeignClient(nameuser-service)publicinterfaceUserClient{PostMapping(/api/admin/sysUser/getInfo)RespUserDtogetUserInfo(RequestBodyInternalReqreq);}但接口的实际实现是被调用方的 Controller。当被调用方改了接口路径、参数类型、返回格式时调用方的 Feign 接口不会有任何编译期提示——只有在运行时调用失败才会发现问题。对于服务数量多、迭代频繁的项目这种割裂感会非常痛苦。三、InternalServiceClient 的设计3.1 核心设计理念MetaLite 的InternalServiceClient设计哲学很明确一个统一的调用入口自动处理所有横切关注点。publicclassInternalServiceClient{publicvoidcallOneInstance(RpcRequestrpcRequest){}publicTRespTcallOneInstanceRtnData(RpcRequestrpcRequest,ClassTrespDataType){}publicTRespListTcallOneInstanceRtnListData(RpcRequestrpcRequest,ClassTrespDataType){}publicERespPageResultDtoEcallOneInstanceRtnPageData(RpcRequestrpcRequest,ClassErespDataType){}}只有 4 个核心方法覆盖所有调用场景方法用途callOneInstance调用单个实例无返回值callOneInstanceRtnData调用单个实例返回单个对象callOneInstanceRtnListData调用单个实例返回集合callOneInstanceRtnPageData调用单个实例返回分页数据3.2 上下文自动传递这是 InternalServiceClient 最核心的能力之一。在checkAndFillRpcRequest方法中自动注入所有上下文privateRpcRequestcheckAndFillRpcRequest(RpcRequestrpcRequest){// 构建内部请求头自动传递全链路上下文MapString,StringheadersnewHashMap();headers.put(TRACE_ID,ThreadContext.getTraceId());headers.put(LOGIN_USER_ID,ThreadContext.getLoginUserId());headers.put(APP_ID,ThreadContext.getAppId());headers.put(SEATA_XID,ThreadContext.getSeataXid());if(rpcRequest.getHeaders()null){rpcRequest.setHeaders(headers);}else{rpcRequest.getHeaders().putAll(headers);}returnrpcRequest;}开发者不需要关心 Header 怎么传、TraceId 怎么带过去、Seata XID 怎么跨服务传播——只要用 InternalServiceClient这些全部自动处理。3.3 超时逐层递减这是 InternalServiceClient 区别于 Feign 的关键设计。在checkAndFillRpcRequest中// 超时时间逐层递减inttimeoutMillisrpcRequest.getTimeoutMillis();timeoutMillistimeoutMillis0?API_REQUEST_TIMEOUT_MILLIS:timeoutMillis-API_REQUEST_TIMEOUT_MILLIS_MINUS;rpcRequest.setTimeoutMillis(timeoutMillis);什么意思A → B → C → D A 发起调用时超时 5000ms B 收到调用时超时 5000 - 500 4500ms C 收到调用时超时 4500 - 500 4000ms D 收到调用时超时 4000 - 500 3500ms每经过一层超时时间自动递减。这保证了调用链越长下游超时越短避免下游服务长时间等待最终用户等待时间有上限不会出现调用链累积导致的超长等待下游服务更容易快速失败减少雪崩风险3.4 基于 Nacos 服务发现InternalServiceClient 底层通过 Nacos 做服务发现调用时指定服务名即可RpcRequestrpcRequestRpcRequest.builder().provider(user-service)// 服务名Nacos 自动发现.endpoint(/api/admin/sysUser/getInfo).param(internalReq).rpcMode(RpcModeEnum.HTTP).build();RespUserDtorespinternalServiceClient.callOneInstanceRtnData(rpcRequest,UserDto.class);不需要 Feign 那样的FeignClient(name user-service)注解绑定服务名。服务名在每次调用时显式指定清晰且可控。3.5 响应类型安全Feign 返回的是接口定义的类型但类型转换在运行时才校验。InternalServiceClient 要求在调用时显式指定响应数据类型// 单个对象RespUserDtorespinternalServiceClient.callOneInstanceRtnData(rpcRequest,UserDto.class);// 集合RespListOrderDtorespinternalServiceClient.callOneInstanceRtnListData(rpcRequest,OrderDto.class);// 分页RespPageResultDtoUserDtorespinternalServiceClient.callOneInstanceRtnPageData(rpcRequest,UserDto.class);内部会严格校验类型paramCheck(!Collection.class.isAssignableFrom(respDataType),respDataType,不能是集合类型);如果类型不匹配在调用方就会立即报错而不是在下游服务里静默失败。3.6 切面链增强InternalServiceClient 的所有方法都被ApiCallAspect拦截Around(execution(public * com.metalite.rpc.InternalServiceClient.call*(..)))privateObjectaroundBaseInternalServiceClient(ProceedingJoinPointpjp)throwsThrowable{// 统一的日志、监控、异常处理}这意味着每次 RPC 调用都会自动经过日志记录调用方、被调方、耗时、响应码异常处理统一封装为Resp格式监控埋点调用成功率、延迟分布开发者不需要手动加 try-catch、打日志、做监控——全部自动完成。四、代码对比4.1 Feign 方式// 1. 定义 Feign 接口FeignClient(nameuser-service,fallbackUserClientFallback.class)publicinterfaceUserClient{PostMapping(/api/admin/sysUser/getInfo)RespUserDtogetUserInfo(RequestBodyInternalReqreq);}// 2. 写 Fallback 实现ComponentpublicclassUserClientFallbackimplementsUserClient{OverridepublicRespUserDtogetUserInfo(InternalReqreq){returnResp.error(用户服务暂时不可用);}}// 3. 写 RequestInterceptor 传递上下文ComponentpublicclassFeignHeaderInterceptorimplementsRequestInterceptor{Overridepublicvoidapply(RequestTemplatetemplate){template.header(traceId,ThreadContext.getTraceId());template.header(userId,ThreadContext.getLoginUserId());// ... 每个 Header 都要手动加}}// 4. 调用方使用ResourceprivateUserClientuserClient;publicRespUserDtogetUser(StringuserId){InternalReqreqnewInternalReq();req.putParam(userId,userId);returnuserClient.getUserInfo(req);// 没有超时递减}4.2 InternalServiceClient 方式ResourceprivateInternalServiceClientinternalServiceClient;publicRespUserDtogetUser(StringuserId){InternalReqreqnewInternalReq();req.putParam(userId,userId);RpcRequestrpcRequestRpcRequest.builder().provider(user-service).endpoint(/api/admin/sysUser/getInfo).param(req).rpcMode(RpcModeEnum.HTTP).build();// 一行调用上下文自动传、超时自动减、响应自动转换returninternalServiceClient.callOneInstanceRtnData(rpcRequest,UserDto.class);}对比一下维度FeignInternalServiceClient接口定义需要定义 Feign 接口 Fallback 类不需要直接构造 RpcRequest上下文传递需要手动写 RequestInterceptor自动注入超时控制全局配置或每个 Client 单独配逐层自动递减降级处理每个接口写 Fallback 实现响应体统一处理类型安全运行时校验调用时显式指定监控日志需要额外配置切面自动处理五、总结设计哲学禁止 Feign 不是因为我们觉得 Feign 不好而是因为Feign 解决的是能不能调的问题但微服务更需要的是调得好的问题。InternalServiceClient 的设计哲学约定优于配置上下文传递、超时递减、切面监控全部自动不需要开发者操心显式优于隐式服务名、端点、响应类型每次调用都显式指定不留隐患统一入口优于分散定义一个 Client 覆盖所有调用场景不需要为每个服务写单独的接口这不是对 Feign 的否定而是对微服务调用本质的另一种理解。InternalServiceClient 的这套设计哲学贯穿了整个 MetaLite 框架——约定优于配置、显式优于隐式、统一入口优于分散定义。框架简介元界 MetaLite — 下一代企业级 Java 微服务技术底座作者简介基于 Spring 体系 15 年企业级开发经验专注于通过企业级生产环境落地的工程思维和架构思想打造下一代Java 微服务技术底座完整文档与源码Gitee 搜索 MetaLite https://gitee.com/MetaLite
返回列表