[toc]
概述
hzero 的微服务体系里, 服务除了要注册到注册中心(nacos 等), 还注册到 admin 服务(平台治理服务);
admin 服务接收到业务服务的注册信息, 负责完成路由刷新、权限刷新、swagger 信息刷新, 任意一项刷新失败都会导致服务出现异常现象;
由于刷新是异步执行, 且涉及的服务较多, 出现问题通常不好排查; 详细了解注册流程有助于快速排查定位;
注册流程图:
注册成功的表现:
服务没有不停地报错
swagger 中可以看到服务并且可以调用接口;
注册失败时的排查方法
检查注册流程每个环节, 与之对应的排查过程:
代码调用过程
业务服务:
业务服务依赖了 jar 包hzero-boot-admin.jar
, 其中注册了自动配置:AdminRegistrationAutoConfiguration
, 容器启动时自动执行:
AdminAutoRegistration#start //SmartLifecycle.start
AdminAutoRegistration#register
retryAlwaysUntilSuccess //死循环, 从一启动就开始注册, 直到成功
AdminTransport#transport 注册到admin
AdminTransport#requestAdmin
//请求的地址: post 8061端口: /actuator/service-init-registry/register
//requestBody: org.hzero.boot.admin.registration.Registration 实例
response = (Response)this.httpTransporter.transport(new StaticEndpointHttpRequest(instance, staticEndpoint, requestBody, Response.class));
复制代码
其中的注册接口:
StaticEndpoint#ADMIN_SERVICE_REGISTER
ADMIN_SERVICE_REGISTER("/actuator/service-init-registry/register", HttpMethod.POST, true, "HZERO治理服务注册服务的api接口"),
复制代码
doc 接口:
/v2/iosp/api-docs
...swagger.controller.CustomController.getDocumentation
/v2/api-docs
springfox.documentation.swagger2.web.Swagger2Controller.getDocumentation
复制代码
admin 服务:
业务服务调用 admin 的注册接口actuator/service-init-registry/register
:
ServiceInitRegistryEndpoint#register
RestServiceInitRegistry#register
assertHealth(service); //只判断服务的状态, 如果up就把服务添加到启动成功的列表里
serviceInitRegistryRepository.add(service);
复制代码
其中的ServiceInitRegistryRepository
接口:维护需要初始化(未刷新路由、权限、swagger)的服务信息; 具体实现:RedisServiceInitRegistryRepository
, 存储在 redis db1 hadm.services-to-init
;
其中的RestServiceInitRegistry
类:
public class RestServiceInitRegistry implements ServiceInitRegistry, SmartLifecycle { ...
RestServiceInitRegistry#start //SmartLifecycle.start, 应用启动成功后自动执行;
RestServiceInitRegistry#doStart
this.initExecutor = new ThreadPoolExecutor( ... //服务刷新执行器, 执行刷新任务;
this.checkExecutor = new ThreadPoolExecutor( ... //检查执行器, while循环检查, 负责清理已下线的过期服务、如果检查到未刷新服务唤醒服务刷新线程(initExecutor)
复制代码
initExecutor
的流程;
RestServiceInitRegistry#doStart
this.initExecutor.execute(() -> {
init(getUnInitializedServices()) //刷新未初始化的服务
ServiceInitRegistry#init
RestServiceInitRegistry#doInit
RestServiceInitRegistry#executeInit //初始化链, 调用刷新链
buildInitChain().doChain(context);
//依次执行InitFilter的实例
RouteInitFilter
PermissionInitFilter
SwaggerInitFilter
复制代码
任务最终执行是责任链/过滤链模式, 相关代码: ...admin.infra.chain
包、InitFilter、InitChain、DefaultInitChain、InitChainFactoryBean
权限刷新
自动刷新权限
服务注册后自动刷新权限流程:
admin 服务:
PermissionInitFilter#doFilter
PermissionRefreshService#innerRefresh
v1/tool/permission/inner/fresh
ACTUATOR_PERMISSION("/v2/actuator/permission", HttpMethod.GET, false, "获取服务权限信息"); //注意端口不是管理端口, 是服务端口;
复制代码
iam 服务:
ToolPermissionController#innerRefresh
IDocumentServiceImpl#refreshPermissionAsync
IDocumentServiceImpl#refreshPermission
ParseServicePermissionImpl#parser
List<Permission> permissions = this.permissionHandler.handle(serviceName, instance);
AbstractPermissionHandler#handle
ActuatorPermissionHandler#doHandle
fetchPermissionDataByIp
复制代码
手动刷新权限
开发管理 - 系统工具 - 刷新权限, 调用/iam/v1/tool/permission/fresh
的过程:
iam 服务:
ToolPermissionController#refresh
IDocumentServiceImpl#refreshPermission
ParseServicePermissionImpl#parser
AbstractPermissionHandler#handle
ParseServicePermissionImpl#processPermissions
复制代码
ParseService#parser
的 javadoc:
解析权限步骤:
判断是否要跳过解析服务权限,默认跳过 register, gateway, oauth
调用服务接口 /v2/choerodon/api-docs 获取服务 swagger json 文档
从 json 中解析权限
如果权限编码重复,加上 HttpMethod 后缀
保存权限,编码存在则更新,不存在则新增
如果要清除过期权限,则清除过期权限
缓存权限到 Redis,默认存储到 db4>gateway:permissions
实测: 手动刷新权限不会更新HADM_SWAGGER
表里的数据; 程序每次启动的时候会更新HADM_SWAGGER
并且刷新权限;
权限刷新报错: parse_permission_data.failure
症状: 【系统工具】-【刷新权限】报错:
hiam.error.parse_permission_data.failure
复制代码
原因: iam 刷新权限时获取的结果是乱码, 导致解析失败;
乱码原因: jhipster 的 prod 模式开启了 http 响应压缩, 返回的数据被压缩, response header 里包含: content-encoding: gzip
;restTemplate 没有兼容压缩的情况;关闭压缩: server.compression.enabled: false
Using JHipster in production
路由刷新
路由配置
ChoerodonRouteData choerodonRouteData = new ChoerodonRouteData();
choerodonRouteData.setName(environment.getProperty("hzero.service.current.name", "demo"));
choerodonRouteData.setPath(environment.getProperty("hzero.service.current.path", "/demo/**"));
choerodonRouteData.setServiceId(environment.getProperty("hzero.service.current.service-name", "demo"));
复制代码
choerodonRouteData.name: 路由名称, gatewayRoute 的标识,对应 gatewayRoute 的 id 字段
choerodonRouteData.path: 路由的路径;
choerodonRouteData.serviceId: 通过实测、查看代码, 业务服务的这个配置无效; 虽然无效, 但是也不能不设置, 否则程序启动报错: bean 实例化失败;
//admin刷新路由的时候, 会把路由配置里的serviceId设置为serviceName:
ParseRouteServiceImpl#executeRefreshRoute
data.setServiceId(serviceName);
复制代码
如果开启了 context-path, 路由配置需注意
//如果开启了context-path, 这里的路径必须和context-path保持一致
choerodonRouteData.setPath(environment.getProperty("hzero.service.current.path", "/demo-qxx/**"));
//这里设置为false
choerodonRouteData.setStripPrefix(false);
复制代码
业务服务路由的 name 或 path 重复
新服务注册的路由如果和已有服务重复, 新服务路由注册失败, 但服务的日志里没有任何提示, swagger 里看不到服务, 普通开发者很难排查问题;admin 服务里会 info 级日志:
ParseRouteServiceImpl#executeRefreshRoute
LOGGER.info("route conflict, try to modify it on the interface, cause: {}", cause.toString());
复制代码
最终采用的解决办法: 服务启动时路由注册成功才能启动(hzero 原版检查健康状态为 up 就能启动)
手动刷新路由报错: ExceptionResponse: For input string: "80,80"
dev 环境正常, prod 环境访问服务时报错error.permission.routeNotFound
, 路由管理能看到路由, 手动刷新路由报错:
refresh service route error, serviceName=hw-platform, ex=fetch failed, instance: hw-platform, ex=http transport failed, ExceptionResponse: For input string: "80,80"
复制代码
admin 报错代码:
StringHttpTransporter#transport
ExceptionResponse response = objectMapper.readValue(body, ExceptionResponse.class);
if (response != null && response.getFailed()) {
throw new RestClientException("http transport failed, ExceptionResponse: " + response.getMessage());
}
复制代码
业务服务报错:
java.lang.NumberFormatException: For input string: "80,80"
at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) ~[na:1.8.0_272]
at java.lang.Integer.parseInt(Integer.java:580) ~[na:1.8.0_272]
at java.lang.Integer.parseInt(Integer.java:615) ~[na:1.8.0_272]
at ....swagger.controller.CustomController.componentsFrom(CustomController.java:145) ~[starter-core-2.0.0.RELEASE.jar!/:2.0.0.RELEASE]
复制代码
代码位置:
...swagger.controller.CustomController#componentsFrom
String port = request.getHeader("X-Forwarded-Port");
if (hasText(port)) {
builder.port(Integer.parseInt(port));
}
复制代码
报错原因: X-Forwarded-Port=80,80
;修复方法: 加 try..catch, 重置为-1;
评论