转至元数据结尾
转至元数据起始

CASE1:优付公众号上的自由职业者签约

优付公众号上的自由职业者签约——用户关注我司公众号,访问公众号里的自由职业者签约H5,在这个H5页面上完成自由职业者签约。

程序实现逻辑是,页面通过微信公众号静默授权方式获取openId后,携带openId调用后端“获取自由职业者信息”接口。

后端程序是SSM框架:SohoSignController → SohoSignService → SohoSignDAO → SohoSignMapper.xml

@RestController class SohoSignController {
	@Autowired SohoSignService sohoSignService;

	@GetMapping("/sohosign")
	public Result sohoSign(String openId) {
		Map<String, Object> mapQuery = new HashMap<String, Object>();
		mapQuery.put("openId", openId);
		return Result.success(sohoSignService.selectSohoList(mapQuery));			
	}
}

@Service class SohoSignService {
	@Autowired SohoSignDAO sohoSignDAO;
	
	public List<Soho> selectSohoList(Map<String, Object> mapQuery) {
		return sohoSignDAO.selectSohoList(mapQuery);
	}
}

interface SohoSignDAO {
	List<Soho> selectSohoList(Map<String, Object> mapQuery);
}

SohoSignMapper.xml
    <select id="selectSohoList" parameterType="java.util.Map" resultMap="BaseResultMap">
        select *
        from t_soho a
        <where>
            <if test="mobile != null">
                and a.mobile = #{mobile}
            </if>
            <if test="openId != null">
                and a.open_id = #{openId}
            </if>
        </where>
        order by a.id desc
    </select>
t_soho表有300w+数据量,open_id字段上有索引。每个open_id存在10多条数据记录。因此,按openId查询数据没有性能问题。

代码看似正常。

突然某几天里,优付这个签约服务tomcat频繁出现假死和OOM。

后来经排查,才知道,有人直接访问了这个签约url——是的,没有传openId参数——这意味着什么?技术敏感的同学必然有答案。


CASE2:老风控的一个不起眼的定时任务,引发蝴蝶效应

老risk里有一个定时任务,每天凌晨4点跑。程序逻辑里,有一段代码是根据用户的身份证号获取用户姓名。见下方第2个方法 getUserNameByIdCard ,所调用的UserAPI#list在 zhenghe-provider 服务里。

-- com.emax.risk.provider.modules.riskstrategy.provider.BaseRiskStrategyService#handleRiskWarningPerson
for (TotalMerSohoMonth sohoMonth: personList) {
    ...
    warning = new RiskPersonalWarning();
    warning.setIdCard(sohoMonth.getIdCard());
    //获取userName
    String userName = riskCommonService.getUserNameByIdCard(sohoMonth.getIdCard());
    if (StringUtils.isNotEmpty(userName)){
        warning.setUserName(userName);
    }
    ...
}


-- com.emax.risk.provider.modules.riskauth.RiskCommonService#getUserNameByIdCard
public String getUserNameByIdCard(String idCard){
    //先查数据中心,数据中心查不到再查整合
    List<SbhSoho> sbhSohoList = sbhSohoManager.list(new LambdaQueryWrapper<SbhSoho>()
            .eq(SbhSoho::getIdCard, AesEncryptUtil.encrypt(idCard)));
    if (CollectionUtils.isNotEmpty(sbhSohoList)){
        return sbhSohoList.get(0).getName();
    }

    Result<List<UserDTO>> listResult = userAPI.list(new UserQuery().setIdcardNo(idCard));
    if (listResult.isSuccess()){
        List<UserDTO> result = listResult.getResult();
        if (CollectionUtils.isNotEmpty(result)){
            return result.get(0).getUserName();
        }
    }
    return "";
}


在 getUserNameByIdCard 方法中,SbhSohoManager#list操作的数据表 sbh_soho 有 13,602,397条数据,UserAPI#list操作的数据表 t_user 有 3,988,931 条数据。 这两个表均有10多个字段,并且在身份证号字段上均有索引。


可是,然而,but,however,正是这段代码的不严谨,导致 zhenghe-provider 服务出现假死。在假死期间,bosskg的结算下发调用zhenghe-provider的协议校验接口失败,导致用户结算失败。


这段代码有什么问题?问题出在 getUserNameByIdCard 方法未对入参做非空校验。

题外顺便问一句:getUserNameByIdCard 方法中所调用的SbhSohoManager#list,会查 sbh_soho 表的全量数据吗?

CASE3:渠道商docker容器内存不足故障

上周四晚间(20240628)渠道商发版的时候,出现了一个奇怪的现象。agent-system 所在的POD出现假死,重启后,过几分钟又假死。


经分析agent-system服务的日志,发现有好几个大文本日志,原来是调用 AreaAPI时,未指定请求参数,导致返回了全量的4.7万条area数据。

agent-system服务设置的容器运行内存上限是1.5G(spec.containers[].resources.limits.memory = 1500Mi),POD内存不足导致容器中的agent-system程序崩溃或无法正常运行。


下面是 agent-system 里获取area数据的这几行代码,这是一个RestAPI,根据特定条件(页面传参)调用 zhenghe-provider 的 AreaAPI 查询行政地区数据。

@PostMapping("/list")
@ApiOperation(value = "根据条件获取地区列表")
public Result<List<PubAreaVO>> getAreasByVO (@RequestBody PubAreaVO vo) {
    return areaAPI.list(vo);
}

系统业务方面,是渠道商的收款银行卡在本次迭代发版中新增了所在城市的功能。在渠道商列表页面,编辑渠道商数据时,弹出框窗体会自动带出来所属省份和城市。

之所以会出现“查询地区全量数据”这种情况, 是由于存量的渠道商收款银行卡数据记录的地区字段值是null,而上面的RESTAPI 程序未校验空值数据,直接请求了 zhenghe-provider 的RPC接口。碰巧,zhenghe-provider 的 AreaAPI#list里,也没有对入参做必要的非空判断,进而查了全表数据。

@Override
public Result<List<PubAreaVO>> list(PubAreaVO vo) {
    LambdaQueryWrapper<PubArea> wrapper = getWrapperByVO(vo);
    List<PubArea> list = pubAreaManager.list(wrapper);

    List<PubAreaVO> result = new ArrayList<>();
    list.forEach(pubArea -> {
        PubAreaVO pubAreaVO = BeanMapper.map(pubArea, PubAreaVO.class);
        result.add(pubAreaVO);
    });
    return Result.success(result);
}



原罪:上面3个情况,都败在了基本的数据校验。

那么,先来个灵魂拷问三连击↓

  1. 如何修正我们的程序?
  2. 如何全面修正我们的程序?
  3. 如何更彻底地规避这类因全表查询所带来的BUG?















接下来,我们逐个解答。


🍀 1. 如何修正我们的程序?

哪里出问题,就解决哪里。

对于CASE1,入口对openId做参数判空。

对于CASE2,同CASE1。

对于CASE3,同CASE2。

🍀 2. 如何全面修正我们的程序?

按上面 🍀1 的修正方式,调用入口是修正了,那,其他的调用入口呢?

就是说,两头都要修正。

对于RPC接口,接口服务提供者自然是要做参数校验的。——这是你的职责,如果以“调用方做了判空了,我这里就不用了”,那你太单纯了,单纯到不靠谱。

🍀 3. 如何更彻底地规避这类因全表查询所带来的BUG?

按上面 🍀2 的修正方式,好像可以高枕无忧了,按说也挺全面了。

如果我再提个问题,也许会让你头疼———— 上面CASE涉及到的大数据量表 t_soho、sbh_soho、t_user、pub_area 这些表,修正后的程序引发OOM的概率小了, 那么,我们的系统里还存在着 诸如 task_apply、order_detail、tax_user_sign 等等这些 大数据量表, 这些表的程序 是不是也会引发OOM呢?

会的,一定会!

该-怎么办?





MybatisSqlSelectLimitInterceptor 对PreparingSQL整体拦一道。






2024-07-25

I have a dream. -CQRS



REF:

2023-8-11-OOM-爱人才tomcat-web-JVM:生命只是大梦一场

未对参数做非空校验,我的服务被搞得内存溢出(OOM)了!

编写评论...