Java 系统稳定性进阶之路:策略、实践与优化

前言

最近在做系统的稳定性治理,有一点心得和感悟,分享、记录和输出一下,走几步要回头看看,沉淀一下。

稳定性建设-八股文

  • H-热点(Hotkey)
  • R-限流(RateLimiter)
  • A-授权(Authorize)
  • B-隔离(BulkHead)
  • C-熔断(CircuitBreaker)(服务熔断+服务降级)

之前一个阿里师兄分享过上面的一个关键词,不过稳定性建设无时无刻不存在,本文以自己遇到且落地的案例为例,讲解一下系统的稳定性建设。

稳定性建设-充分利用机器资源

简单粗暴的说,穷则加并发,富则加机器。本文主要讨论加并发。

案例场景

在电商平台购物场景中,当出现非预期场景时最开始直接与买家或者卖家沟通的客服,比如退货不退款、假货投诉等等,客服的回答与服务直接影响用户的心智,影响用户的服务体验。因此监测客服与用户的聊天记录中是否存在违规行为就显得尤为重要。

举几个常见的场景:

  • 禁止辱骂用户
  • 禁止长时间不回复用户
  • 要求给客户推荐某商品

针对以上三个场景,其实是三个不同的算法模型去处理识别,当然生产模型有更多。

案例问题

从业务角度来说,识别一次聊天会话是否存在违规行为。
从开发角度来说,入参是一个聊天记录明细和模型集合(多租户),出参是有哪些违规行为。

伪代码如下

    /**
     * @param chatList    聊天明细
     * @param modelIdList 模型集合
     * @return
     */
    public Object chatCheck(List<String> chatList, List<Integer> modelIdList) {
    
    
        for (Integer modelId : modelIdList) {
    
    
            List<String> deepCopyChatList = new ArrayList<>(chatList);
            //处理业务逻辑
            doChatCheck(deepCopyChatList, modelId);
        }
        return "demo";
    }
    public Object doChatCheck(List<String> chatList, Integer modelId) {
    
    
        //处理业务逻辑
        return null;
    }

整个接口的RT超时时间竟然是秒级,完全不符合一个中台的毫秒的要求。接口超时严重。

解决方案

穷则加并发,富则加机器。本文选择穷办法。

修改后的代码如下。当然不推荐使用parallelStream做并发哈,想偷懒写了。

  /**
     * @param chatList    聊天明细
     * @param modelIdList 模型集合
     * @return
     */
    public Object chatCheck(List<String> chatList, List<Integer> modelIdList) {
    
    
        
        modelIdList.parallelStream() //多线程
                .forEach(modelId -> {
    
    
            List<String> deepCopyChatList = new ArrayList<>(chatList);
            //处理业务逻辑
            doChatCheck(deepCopyChatList, modelId);
        });
        return "demo";
    }
    public Object doChatCheck(List<String> chatList, Integer modelId) {
    
    
        //处理业务逻辑
        return null;
    }
收益

接口的RT从秒级降低为毫秒级,接口失败数量降低到单位数(反正贼夸张)。

稳定性建设-灰度能力

找人做小白鼠,跑跑看

案例场景

在开发中常常会因为下游接口要升级需要你配合改接口,以这种场景为例。直接切肯定是有问题的,肯定要做灰度。比如这一批人走新接口,剩下的还是走老接口。

解决方案

假设每一次请求都会携带用户的ID,即userId,那么我的灰度规则为userId%100<1,即圈选1/100的用户当小白鼠。

public class SpelUtils {
    
    

    /**
     * 根据用户ID进行取模运算的SPEL工具方法
     *
     * @param spel SPEL表达式
     * @param userId 用户ID
     * @param modulo 取模的数值(除数)
     * @return 取模后的结果
     */
    public static boolean grayMatchAbility(String spel,long userId, int modulo) {
    
    
        // 创建SPEL表达式解析器
        ExpressionParser parser = new SpelExpressionParser();
        // 构建SPEL表达式,这里表示对传入的变量进行取模操作
        Expression expression = parser.parseExpression("(#userId % #modulo) < 1");
        // 创建求值上下文,用于设置表达式中的变量值
        EvaluationContext context = new StandardEvaluationContext();
        context.setVariable("userId", userId);
        context.setVariable("modulo", modulo);
        // 对表达式求值并返回取模结果(这里强制转换为int类型,根据实际情况可能需要调整)
        return (boolean) expression.getValue(context);
    }

    public static void main(String[] args) {
    
    
        long userId = 123456L;
        int modulo = 10;
        //从配置文件读取
        String spel ="(#userId % #modulo) < 1";
        boolean result = grayMatchAbility(spel, userId, modulo);
        System.out.println("用户ID " + userId + " 取模 " + modulo + " 的结果是: " + result);
    }
}
收益

少出线上问题,就这一条足以。

稳定性建设-隔离

不要把鸡蛋放一个篮子里

场景&解决方案
  • 服务隔离:https://cbeann.blog.csdn.net/article/details/134362757
  • DB隔离:https://cbeann.blog.csdn.net/article/details/134362757
  • 消息隔离:https://cbeann.blog.csdn.net/article/details/134362757
  • 认证key隔离:比如访问某个下游接口,图省事拿别的系统的key去写逻辑,如果下游接口是根据key做限流,那么此时两个人用一个key可能触发限流
收益
  • 个性化限流,集群级别的限流基本都是支持的,如果在参数级别限流,其实不好搞
  • 收费透明,现在都是云服务,不同租户用自己的会更清晰一些。
  • 数据安全,如果量级较大其实会存在托库的链路(MySQL同步到Hive),大家在一个库你把我数据拿走不安全。

稳定性建设-热点数据

加缓存,前提是能接受短期不一致

案例场景

没用过的叉出去,略。

定位问题

正常情况下是不加缓存,因为有别人触发你(告诉你接口慢等等)你才会优化,加缓存。那你如何知道是哪里慢从而加的缓存呢?

  • IO密集型:通过arthas的trace命令查看接口慢的地方
  • CPU密集型:每一个方法都很短,但是量大,火焰图无敌。如下图所示:长度越长,执行时间越长
    在这里插入图片描述

稳定性建设-大key问题

Redis有大key,本地缓存也有大key,严重会出现OOM

案例场景

在电商平台购物场景中,当出现非预期场景时最开始直接与买家或者卖家沟通的客服,比如退货不退款、假货投诉等等,客服的回答与服务直接影响用户的心智,影响用户的服务体验。因此监测客服与用户的聊天记录中是否存在违规行为就显得尤为重要。

举几个常见的场景:

  • 禁止辱骂用户
  • 禁止长时间不回复用户
  • 要求给客户推荐某商品

针对以上三个场景,其实是三个不同的算法模型去处理识别,当然生产模型有更多。

其实这个场景是在稳定性建设-充分利用机器资源稳定性建设-热点数据衍生出来的一个场景:加并发且加缓存

案例问题

系统出现了OOM,脱敏代码如下

    @Cacheable(value = "user")
    public Object getUser(List<Object> params) {
    
    
        // dao.update(user);
        return Object;
    }
定位原因

上述代码中因为没有设置缓存key,默认是方法名-参数名-参数JSON字符串。如果你的入参中有视频二进制,那么Key贼大。
解决方案:Key做一下MD5,过期时间设置的短一些。

稳定性建设-监控埋点

监控埋点:在SpringBoot自定义指标并集成Prometheus和Grafana监控

稳定性建设-熔断

案例场景

大模型比较火,但是这种接口的RT会贼高。比如下面图片。输入太多就直接给禁掉了,后端同理
在这里插入图片描述

解决方案

对入参做统计,比如超时1W字的直接不处理,直接返回错误码,别把机器打挂。

稳定性建设-批处理

定时任务处理订单下线,需要执行

update order set status = 1 where id = 1

此时可修改为

update order set status = 1 where id in (1,2,3,...n)

注意:in别太多

稳定性建设-可溯源

我们查询飞书会调用用户请假,我们对于飞书来说是一个租户,一个秘钥key。
飞书根据租户(秘钥key)限流,但是出现的问题是我自己已经不请求了,但是我偶尔请求一次还是限流,所以想找飞书看一下到底发送请求的IP是哪个应用,但是飞书没有这样的能力。
所以调用别人接口要有标识。

总结

稳定性建设非一蹴而就,需要长期观察才能发现问题,短期收益可以搭建监控面板。