美文网首页
轻量微服务基础架构集成

轻量微服务基础架构集成

作者: more2023 | 来源:发表于2022-08-08 17:06 被阅读0次

    一、背景

    在微服务大行其道的今天,微服务思想,无处不在的影响着软件开发的各个阶段。作为微服务的实践者,在微服务的使用过程中,一边享受着微服务带来的舒爽和放飞,也一边承受着微服务带来的痛点。本文主要围绕微服务使用的痛点展开讨论,并从微服务基础架构出发,给出可行的基础架构集成解决方案。在微服务使用过程中,经常遇到的痛点有如下几点:
    1、随着微服务模块的增多,微服务基础框架,需要更好的支撑maven打包瘦身,多模块依赖瘦身。
    2、微服务集成了多样化的业务,微服务基础框架,需要在统一认证和鉴权中心,支持多样性的认证。如:oauth2四种认证模式、用户名|密码|验证码 认证模式、手机号|密码 认证模式、openid认证等模式。
    3、随着微服务模块的增多和服务依赖错综复杂,微服务基础框架,需要更好的支撑开发过程中,本地服务和测试环境服务之间联调,不会出现服务乱窜和冲突,得以提高开发效率。同时新产品特性上线,如何通过配置化,更简单地支撑灰度发布切量。
    4、微服务服务调用链路过长,参与人员过多,针对异常问题,微服务基础框架,需要有全链路日志追踪,能够帮助运营和开发人员快速定位和处理问题。
    5、分布式环境中,对于强一致性的业务场景,微服务基础框架,需要实现并发和协同的分布式锁、分布式锁超时续约等通用组件支撑相关业务。
    6、微服务全方位的监控,服务器性能监控、JVM监控、日志关键词监控、自定义业务等监控。
    7、分布式事务组件。
    8、微服务服务依赖错综复杂,微服务基础框架,需要更好的阶梯消息推送组件,支撑微服务稳定的链路回调。
    9、分布式任务调度器。
    综上所述、微服务的使用痛点是各色各样,而且复杂繁琐,好在前辈们帮我们趟平了道路,提供了多样性的解决方案。但方案落地到实处,面临一个微服务最大的痛点,钱(资源)。所以本文的微服务基础框架集成,在解决微服务痛点的同时,尽可能兼顾轻量。

    二、技术选型

    1、基础环境
    1.1、jdk11
    1.2、docker
    2、存储
    2.1、mysql
    2.2、redis
    3、消息队列
    3.1、kafka
    3.2、rabbitmq
    4、搜索
    4.1、elasticsearch
    5、微服务
    5.1、spring cloud
    5.2、spring cloud alibaba
    5.3、spring cloud gateway
    5.4、ribbon
    5.5、hystrix
    5.6、nacos(服务注册中心和配置中心)
    5.7、seata
    6、响应式服务
    6.1、vert.x
    7、基础能力
    7.1、filebeat
    7.2、zookeeper
    7.3、logstash
    7.4、prometheus
    7.5、grafana
    7.6、kibana
    8、其他
    8.1、nginx
    8.2、xxl-job

    三、架构设计

    基础框架架构图.png
    上图所示,是轻量微服务基础架构集成设计,整体展现说明包含以下几个方面:
    1、微服务基础架构集成说明
    本轻量微服务基础架构集成设计,包含了Spring Cloud微服务体系、响应式基础服务体系、日志监控预警体系等几大模块,用于支撑微服务应用的基础架构。
    2、Spring Cloud微服务模块说明
    该模块依托Spring Cloud、Spring Cloud Alibaba技术体系,面向互联网设计、提供微服务基本要素和业务开发非功能性支撑组件,支撑业务代码开发、深度定制。
    3、响应式基础服务模块说明
    该模块依托Vert.x技术体系,针对通用的、业务清晰的场景,封装为响应式基础服务,为整个基础架构,提供性能更好、稳定性更高、安全性更强的基础服务功能。如:支付、短信、IM、文件。
    4、日志监控预警模块说明
    该模块主要是针对微服务全方位的监控,服务器性能监控、JVM监控、日志关键词监控、自定义业务等监控。

    四、集成方案

    1、全链路日志解决方案

    1.1、实现思路

    1. 为实现全链路日志追踪,我们针对每一个请求都使用一个唯一标识traceId,来区分每一次请求。并且不修改原有日志的打印方式,代码无侵入,方便存量系统改造。
    2. 建议项目以logback方式进行日志处理,就使用Logback的MDC机制日志模板中加入traceId标识,取值方式为 %{traceId}。
      1.2、MDC方式实现全链路日志实现面临的问题
      查看MCDAdapter接口的实现类LogbackMDCAdapter源码,使用的是ThreadLocal,只对本线程有效。对子线程、下游服务的MDC里的值会丢失,所以需要解决MDC针对子线程、下游服务MDC值传递的问题。
    public class LogbackMDCAdapter implements MDCAdapter {
        **final ThreadLocal<Map<String, String>> copyOnThreadLocal = new ThreadLocal();**
        private static final int WRITE_OPERATION = 1;
        private static final int MAP_COPY_OPERATION = 2;
        final ThreadLocal<Integer> lastOperation = new ThreadLocal();
    
        /** ...... */
    }
    

    1.3、解决方案

    1. 重构LogbackMDCAdapter,引入阿里开源的TransmittableThreadLocal,实现父子线程之间的值传递;
       public class CusTtlMDCAdapter implements MDCAdapter {
         private final ThreadLocal<Map<String, String>> copyOnInheritThreadLocal = new TransmittableThreadLocal<>();
    
         private static final int WRITE_OPERATION = 1;
         private static final int MAP_COPY_OPERATION = 2;
    
       /** ...... */
       }
    
    1. 上下游服务通过拦截器,进行traceId的创建和保存到MDC,上游请求下游服务,通过HTTP请求头携带traceId的方式,进行上下游服务traceId的传输;

    1.4、代码实现细节

    1. 引入Transmittable依赖包;

      说明:实现父子线程值的传递,InheritableThreadLocal也可以,为什么不使用??虽然InheritableThreadLocal也可以实现父子线程之间值的传递,但在线程池模式下,就没办法再正确传递了。TransmittableThreadLocal则即便在线程池模式下,也很好的能进行父子线程值的传递(但TransmittableThreadLocal实现线程池模式下,父子线程传值不出问题,线程池还得用TTL加一层代理)。

        <dependency>
                   <groupId>com.alibaba</groupId>
                   <artifactId>transmittable-thread-local</artifactId>
                   <version>2.12.0</version>
            </dependency>
    
    1. 重构LogbackMDCAdapter

      说明:线程池需要TTL代理一下,则需要

       public class CusTtlMDCAdapter implements MDCAdapter {
         private final ThreadLocal<Map<String, String>> copyOnInheritThreadLocal = new TransmittableThreadLocal<>();
    
         private static final int WRITE_OPERATION = 1;
         private static final int MAP_COPY_OPERATION = 2;
    
       /** ...... */
    
       }
    

    //使用TTL代理一下线程池

       public class CustomThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {
           private static final long serialVersionUID = -5887035957049288777L;
    
           @Override
           public void execute(Runnable runnable) {
               Runnable ttlRunnable = TtlRunnable.get(runnable);
               super.execute(ttlRunnable);
           }
           
           @Override
           public <T> Future<T> submit(Callable<T> task) {
               Callable ttlCallable = TtlCallable.get(task);
               return super.submit(ttlCallable);
           }
           
           @Override
           public Future<?> submit(Runnable task) {
               Runnable ttlRunnable = TtlRunnable.get(task);
               return super.submit(ttlRunnable);
           }
           
           @Override
           public ListenableFuture<?> submitListenable(Runnable task) {
               Runnable ttlRunnable = TtlRunnable.get(task);
               return super.submitListenable(ttlRunnable);
           }
           
           @Override
           public <T> ListenableFuture<T> submitListenable(Callable<T> task) {
               Callable ttlCallable = TtlCallable.get(task);
               return super.submitListenable(ttlCallable);
           }
       }
    
    1. 通过过滤器进行traceId的新增和保存
       @ConditionalOnClass(value = {HttpServletRequest.class, OncePerRequestFilter.class})
       @Order(value = MDCTraceUtils.FILTER_ORDER)
       public class WebTraceFilter extends OncePerRequestFilter {
           @Resource
           private TraceProperties traceProperties;
    
           @Override
           protected boolean shouldNotFilter(HttpServletRequest request) {
               return !traceProperties.getEnable();
           }
           
           @Override
           protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                           FilterChain filterChain) throws IOException, ServletException {
               try {
                   String traceId = request.getHeader(MDCTraceUtils.TRACE_ID_HEADER);
                   if (StringUtils.isEmpty(traceId)) {
                       MDCTraceUtils.addTraceId();
                   } else {
                       MDCTraceUtils.putTraceId(traceId);
                   }
                   filterChain.doFilter(request, response);
               } finally {
                   MDCTraceUtils.removeTraceId();
               }
           }
       }
    
    1. feign发送http请求,设置请求头,添加traceId
    @Configuration
    @ConditionalOnClass(value = {RequestInterceptor.class})`
    public class FeignTraceConfig {`
    
        @Resource
        private TraceProperties traceProperties;
    
        @Bean
        public RequestInterceptor feignTraceInterceptor(){
            return requestTemplate -> {
                //传递token
                ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
                if(requestAttributes != null){
                    String token = requestAttributes.getRequest().getHeader("Authorization");
                    if(StringUtils.isNotEmpty(token)){
                        requestTemplate.header("Authorization", token);
                    }
                }
                if(traceProperties.getEnable()){
                    //传递日志traceId
                    String traceId = MDCTraceUtil.getTraceId();
                    if(StringUtils.isNotEmpty(traceId)){
                        requestTemplate.header(MDCTraceUtil.TRACE_ID_HEADER, traceId);
                        requestTemplate.header(MDCTraceUtil.SPAN_ID_HEADER, MDCTraceUtil.getNextSpanId());
                    }
                }
            };
        }
    }
    
    1. 日志模板设置traceId
       <property name="BI_LOG_PATTERN_NO_COLOR" value="[${APP_NAME}:${ServerIP}:${ServerPort}] %d{yyyy-MM-dd HH:mm:ss.SSS} %level ${PID} [%X{traceId}-%X{spanId}] [%thread] %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}" />
    
    1. 通过AOP,实现请求响应BI日志的拦截封装
    @Slf4j
    @Aspect
    @ConditionalOnClass({HttpServletRequest.class, RequestContextHolder.class})
    public class BiLogAspect {
        @Value("${spring.application.name}")
        private String applicationName;
    
        @Pointcut("execution(public * com.jnc.*.controller.*.*(..)) || execution(public * com.jnc.*.*.controller.*.*(..)) || execution(public * com.jnc.*.*.*.controller.*.*(..))")
        public void pointcut(){
        }
    
        @Around("pointcut()")
        public Object doBiLog(ProceedingJoinPoint joinPoint) throws Throwable {
            //记录开始时间
            LocalDateTime startTime = LocalDateTime.now();
            BiMessage biMessage = new BiMessage();
            biMessage.setBiReqTimestamp(startTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")));
            Object result = null;
            try{
                result = joinPoint.proceed();
            }catch (Exception e){
                result = handleErrorBiLog(joinPoint, biMessage, startTime.toInstant(ZoneOffset.of("+8")).toEpochMilli(), e);
            }finally {
                handleBiLog(joinPoint, biMessage, result, startTime.toInstant(ZoneOffset.of("+8")).toEpochMilli());
            }
            return result;
        }
    
        protected void handleBiLog(ProceedingJoinPoint joinPoint, BiMessage biMessage, Object result, long beginTime){
            setBiMessage(joinPoint, biMessage, beginTime);
            setResult(result, biMessage);
            BiLogger.biLog.info("{}", JsonUtil.toJSONString(biMessage));
        }
    
        protected BaseResp handleErrorBiLog(ProceedingJoinPoint joinPoint, BiMessage biMessage, long beginTime, Exception e){
            setBiMessage(joinPoint, biMessage, beginTime);
            return setErrorResult(e, biMessage);
        }
    
        private void setBiMessage(ProceedingJoinPoint joinPoint, BiMessage biMessage, long beginTime){
            ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
            HttpServletRequest request = requestAttributes.getRequest();
            request.getHeader(MDCTraceUtil.TRACE_ID_HEADER);
    
            String ip = IpUtil.getRemoteAddr(request);
            biMessage.setServerIp(ip);
            biMessage.setTraceId(MDCTraceUtil.getTraceId());
            biMessage.setBiName(applicationName);
            biMessage.setBiPath(request.getRequestURI());
            biMessage.setBiReqContent(getReqParams(joinPoint, request));
            biMessage.setCostTime(System.currentTimeMillis() - beginTime);
        }
    }
    
    1. 请求响应BI日志,接入ELK平台

      通过traceId,可以查询到全链路的请求响应日志,可以更精准的定位全链路业务情况。


      BI日志.png

    2、本地开发依赖测试环境服务联调,服务冲突和实例乱窜解决方案

    2.1、实现思路
    依托Spring Cloud Ribbon客户端负载均衡工具,实现一套自定义负载均衡规则,达到对已知的服务列表进行过滤,留下符合的Server,进而按照一定规则进行选择。

    2.2、实现效果

    1. 测试用户 访问测试服务器页面,请求的所有路由只调用 测试服务器上的实例
    2. 开发A 访问测试服务器,请求的所有路由只调用 开发A本机启动的实例
    3. 开发B 访问测试服务器,请求的所有路由只调用 开发B本机启动的实例

    2.3、实现面临的其他问题
    当测试服务器使用的云服务器,本地启动的实例,注册到Nacos,使用的本地IP,无法和云服务器网络互通,导致负载均衡选择的服务不可用。
    说明:
    可以让公司网管,在公司网络专线,配置本机IP和端口号,外网IP映射,同时本机启动服务,指定服务注册IP为公司外网IP。

    #服务注册为外网IP
    spring:
      cloud:
        nacos:
          discovery:
            ip: 13.18.11.94
    

    2.4、解決方案
    1.自定义AbstractServerPredicate服务器过滤逻辑的基础组件,根据请求头的Tag和Nacos注册实例的元数据tag,进行匹配,筛选出符合要求的实例。

    2.5、代码实现细节
    1.自定义AbstractServerPredicate

    public class MetadataAwarePredicate extends AbstractServerPredicate {
        public static final MetadataAwarePredicate INSTANCE = new MetadataAwarePredicate();
        private final static String KEY_DEFAULT = "default";
    
        @Override
        public boolean apply(@Nullable PredicateKey predicateKey) {
            if(predicateKey == null){
                return false;
            }
            MetadataServer metadataServer = new MetadataServer(predicateKey.getServer());
            //是否支持 Metadata 进行判断
            if(metadataServer.hasMetadata()){
                return doApply(metadataServer, predicateKey.getLoadBalancerKey());
            }
            return false;
        }
    
        
        protected boolean doApply(MetadataServer metadataServer, Object loadBalancerKey){
            String tag;
            if(loadBalancerKey != null && !KEY_DEFAULT.equals(loadBalancerKey)){
                tag = loadBalancerKey.toString();
            }else{
                tag = LoadBalanceContextHolder.getTag();
            }
    
            Map<String, String> metadata = metadataServer.getMetadata();
            String metadataTag = metadata.get("tag");
            //当请求头tag为空,则只选择元数据配置tag为空的服务
            if(StringUtils.isEmpty(tag)){
                if(StringUtils.isEmpty(metadataTag)){
                    return true;
                }
                return false;
            }
    
            if(StringUtils.isEmpty(metadataTag)){
                return false;
            }
            //当请求头有tag,服务有对应的元数据配置,则比较是否一致
            return metadataTag.equals(tag);
        }
    }
    
    1. 新增过滤器,获取请求头Tag,存入本地上下文
    @ConditionalOnClass(Filter.class)
    public class LoadBalanceTagFilter extends OncePerRequestFilter {
        @Value("${" + CommonConstant.LOAD_BALANCE_ENABLE + ":true}")
        private boolean loadBalanceEnabled;
    
        @Override
        protected boolean shouldNotFilter(HttpServletRequest request) {
            return !loadBalanceEnabled;
        }
    
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                        FilterChain filterChain) throws IOException, ServletException {
            try {
                String tag = request.getHeader(CommonConstant.TAG_HEADER);
                if(StrUtil.isNotEmpty(tag)){
                    LoadBalanceContextHolder.setTag(tag);
                }
                filterChain.doFilter(request, response);
            } finally {
                LoadBalanceContextHolder.clear();
            }
        }
    }
    

    3.Spring Cloud Gateway服务,自定义LoadBalancerClientFilter全局过滤器,传递tag服务隔离值

    @Component
    @ConditionalOnProperty(name = CommonConstant.LOAD_BALANCE_ENABLE, havingValue = "true")
    public class LoadBalanceFilter extends LoadBalancerClientFilter {
        public LoadBalanceFilter(LoadBalancerClient loadBalancer, LoadBalancerProperties properties) {
            super(loadBalancer, properties);
        }
    
        @Override
        protected ServiceInstance choose(ServerWebExchange exchange) {
            String tag = exchange.getRequest().getHeaders().getFirst(CommonConstant.TAG_HEADER);
            if (StrUtil.isNotEmpty(tag)) {
                if (this.loadBalancer instanceof RibbonLoadBalancerClient) {
                    RibbonLoadBalancerClient client = (RibbonLoadBalancerClient) this.loadBalancer;
                    String serviceId = ((URI) exchange.getAttribute(GATEWAY_REQUEST_URL_ATTR)).getHost();
                    return client.choose(serviceId, tag);
                }
            }
            return super.choose(exchange);
        }
    }
    

    4.开发人员可以根据请求头Tag和设置Nacos实例元数据,实现指定服务的负载均衡。


    请求头Tag.png Nacos元数据配置.png

    3、响应式服务

    随着 Spring 5 引入对响应式编程的支持,响应式编程模式在 Java 领域中也得到了前所未有的关注,也是一种发展趋势。Java语言说到响应式编程,就不得不说Vert.x框架,Vert.x是一个基于JVM、轻量级、高性能的应用平台,依托于全异步Java服务器Netty,可以快速构建高性能服务器。
    高并发场景下,系统最大的瓶颈是数据IO,突破数据IO瓶颈的常规做法是,对热数据进行远程和本地缓存,同时基于CAP理论,对数据一致性要求不那么严格。除了在数据IO上做突破,在编程框架的选择上和编程方式的优化,也可以有很好的突破。
    3.1、响应式服务实现方案
    Vert.x实现高性能服务,在以往的实践过程中,有两种实现方式,一种是Spring Boot + Vert.x结合的方式;另一种是纯Vert.x生态方式;具体如何取舍,看实际使用场景,下面就这两种实现方式做说明。
    3.1.1、Spring Boot + Vert.x结合的方式
    该方式通过引入Spring,可以无缝接入Spring生态的相关特性和第三方工具,在保障一定性能的前提下,可以更快速的构建高性能服务。缺点是,鉴于Vert.x是多实例的,而对接Spring是单例的,限制了一定的性能,下面是粗略的实现细节。

    Spring_vertx启动类.png
    Spring_vertx入口.png
    Spring_vertx路由分发.png
    Spring_vertx消费.png
    3.1.2、纯Vert.x生态方式
    该方式可以最大化的施展Vert.x框架的特性和性能,多Vert.x实例,compose并行编排非阻塞操作等等,使得异步编程更优雅,更简单。缺点是鉴于Vert.x生态,相比较Spring生态,还有很远的距离,所以很多工具类需要自己造轮子,不能拿来主义。
    vertx启动类.png
    vertx_入口.png
    vertx_业务.png

    4、日志监控预警

    4.1、服务层面监控
    依托Prometheus时序数据库和Grafana可视化工具,对服务节点、Mysql、Nginx、Redis、RabbitMQ、Elasticsearch进行全方位的监控和告警。
    说明:
    借助Prometheus丰富的社区生态,在服务层面通过各种Exporter集成,构建完善丰富的监控样本数据采集,结合Grafana可视化工具,多样性展示指标数据及特定维度数据的告警。

    Exporter列表图.png grafana_微服务监控.png
    4.2、自定义业务监控
    通过全链路BI日志数据,可以根据响应状态码、请求响应耗时等维度日志数据,为日常系统优化、运营策略调整提供支撑数据。
    说明:
    1. 根据全链路请求响应耗时,可以更具针对性的性能优化。
    2. 根据响应状态码,可以为特定业务场景,提供业务运营损耗数据,可以更精准的提供运营策略调整支撑。


      日志分析系统流程图.png
    BI日志存储数据格式图.png

    (未完待续)

    相关文章

      网友评论

          本文标题:轻量微服务基础架构集成

          本文链接:https://www.haomeiwen.com/subject/bbdabrtx.html