如何做好“防御性编码”?

简介:类似于“防御性驾驶”对驾驶安全的重要性,防御性编码目的概括起来就一条:将代码质量问题消灭于萌芽。要做到“防御性编码”,就要求我们充分认识到代码质量的严肃性,也就是“一旦你觉得这个地方可能出问题,那基本它就会(在某个时刻)出问题”。当然,实际情况比这个更严峻。由于大家的编码经验和风格差异,导致大家的意识边界是大小不一的,那些潜伏在意识边界之外的“危险”更加隐蔽和不可琢磨。在意识层面上,我们当然要摒弃“想当然”和“差不多”的思想,严肃评估这些问题发生的可能性,认真对待这些风险。但如若话题止步于此,那其实还是缺乏执行层面的指导意义的,激不起半点“涟漪”的。这个文章目的也更多是关注到“实操层面”的引导

作者 | 字白
来源 | 阿里开发者公众号

一 防御性编码的意义

类似于“防御性驾驶”对驾驶安全的重要性,防御性编码目的概括起来就一条:将代码质量问题消灭于萌芽。要做到“防御性编码”,就要求我们充分认识到代码质量的严肃性,也就是“一旦你觉得这个地方可能出问题,那基本它就会(在某个时刻)出问题”。当然,实际情况比这个更严峻。由于大家的编码经验和风格差异,导致大家的意识边界是大小不一的,那些潜伏在意识边界之外的“危险”更加隐蔽和不可琢磨。

在意识层面上,我们当然要摒弃“想当然”和“差不多”的思想,严肃评估这些问题发生的可能性,认真对待这些风险。但如若话题止步于此,那其实还是缺乏执行层面的指导意义的,激不起半点“涟漪”的。

这个文章目的也更多是关注到“实操层面”的引导。

二 如何防御性编码?

以下需关注的具体方面更多来自于我的习惯和观察,并且统一用伪代码作问题示例。

欢迎大家把自己的“防御性编码心得”在评论区分享出来。

1 并发冲突问题

这个问题在实际项目中,被错误地忽视的比例相当高。它的外在表现形式五花八门,但关键点是:“当你的代码被并发调用时,它会怎么表现?”

我们心里要有个运行时的世界观,代码运行的Context是这样的:多线程 -> 多进程 -> 多机器 -> 多集群。我们编码时,要充分考虑代码在上述世界观多点并发的可能性,及相应的潜在后果。

举几个具体的问题例子):

  • 存在共享变量 或者 数据。(不限于堆内存,也可能是缓存、DB、文件等)
例子1:
有线程 A 和线程 B 两个线程,需要更新「同一条」数据,会发生这样的场景:
1、线程 A 更新数据库(X = 1)
2、线程 B 更新数据库(X = 2)
3、线程 B 更新缓存(X = 2)
4、线程 A 更新缓存(X = 1)
最终 X 的值在缓存中是 1,在数据库中是 2,发生不一致。

例子2:

// 某个 Spring singleton Bean 'aService' 存在一个调用来源标记,记录调用来源是HSF还是HTTP。
// 先 记录来源标记。
aService.setSource(source);
// 再结合source执行其他逻辑。例如将上面记录的source 和 其他参数 插入数据库.
aService.doSomethings(params);
如果这个代码被 HSF和 HTTP 同时调用就会发生问题。
例子3 :
在一个系统中,有两个价格类型 small 和 large,业务逻辑要求 small <= large,且 small 和 large 有2个入口可以分别修改。
目前方案是:对要改变的small或large,增加上面大小关系校验,不通过则拦截,例如 改动small的入口上,校验改后的small <= 系统里的large,不通过则不允许修改。
假如,最新需求要求:修改large的入口继续拦截,但修改small的入口不再拦截,而是发现如果改后small > 系统的large,则将 系统large = 改后的small+0.1,让 约束关系继续成立。 这种改法有问题吗?

答案:这种改法会有问题。即 small这个价格类型存有两个链路同时修改,也是一种并发冲突问题。

举个具体例子:

  • 初始时,系统的small = 2; large = 2;
  • 修改large 链路1:准备将 large 改为 3,检查规则 3(改后large ) >= 2(系统small) 通过。准备写入新的large (3)。
  • 修改small 链路2:准备降 small 改为 4, 发现 4(改后small)> 2(系统large) 不符合规则,则 准备 自动修改 large = 4(改后small)+ 0.1 = 4.1。准备写入 改后small = 4,自动改后 large = 4.1;
  • 如果 链路2 最终先完成写入,链路1再完成写入。则 链路2写入的 large=4.1 会被链路1 写入的large=3 覆盖。最终系统 large =3,而 系统small = 4;破坏了最初的small <= large 的约束。
  • 未考虑集群并发
// 在短信发送服务中,控制对用户的发送频率
timestamp = rateLimitService.getMsgTimestamp(userId);
if( timestamp == null ){rateLimitService.putMsgTimestamp(userId, now);sendMsg(msg);
}else if( timestamp - now > 1 hour ){rateLimitService.putMsgTimestamp(userId, now);sendMsg(msg);
}
这个例子在单机环境执行时没有问题,但线上集群多节点的话,那发送频率的控制就不对了。
  • 非原子操作问题。
// 先查询是否存在目标记录
resultList = dbRepo.list(query);
// 有结果就更新,没有就插入
if( resultList.size() > 0 ){dbRepo.update(xxxx);
} else {dbRepo.insert(xxxx);
}
如果这个代码被多个request 同时执行也会发生问题。
  • 错误的发生并发
单个任务周期性的触发,本来不会有并发问题。
但因单次执行时间变长,导致先后两次执行时间出现重叠。

2 事务问题

对于先A再B后C的这类组合操作,要仔细考虑保障一致性的必要性,做好是否做事务保障的评估。

事务即要求:对一组的operation combo,要保障好执行顺序,保障好context的一致性,保障好结果的一致性。

  • 数据库事务。 发生概率不高,大多会主动预防。
这个问题发生概率倒不高,也比较容易解决。
但要注意,事务执行耗时不要太久,以及避免死锁问题发生。
  • 上下文一致性问题。
以上传并处理Excel文件为例,假如实现分为 2 步:
1、前端调用后端API,上传文件到Server的某个临时目录。
2、前端 在上传完成时,调用后端另一个API,通知 后端处理此文件。

这个例子在集群环境中就会出现概率性成功或失败的情况,集群节点数量越多,失败概率越高。这是因为 前端的前后两次请求调用到了不同节点上,执行上下文出现了不一致。

  • 顺序一致性问题。
常见的,例如对于 ECS运行状态的时序消息,如果下游消费者不是顺序消费,而是并行消费,就可能导致最终记录的状态 与实际不符。

3 分布式锁问题

分布式锁日常也经常用到,在使用细节上存在一些容易忽略的盲点。

  • 获取锁
1、是阻塞式等待锁,还是等不到锁重试,还是等不到锁直接返回。
这个层面主要考量点,这个调用链路对时间和成功率要求是什么。
例如,上游是用户操作,那肯定不能阻塞在等锁那里太久;
2、锁的key设计很关键。
合理设计lock key,能够降低锁碰撞的概率。
例如,你的lock 是加在一个BU层面上,还是加到某个人身上,那冲突概率显然差别很大。
3、对于 持久锁,在循环执行业务逻辑时,要做好锁的状态检查。
RLock lock = redisson.getLock(lock);
lock.lock(-1L, TimeUnit.MINUTES);
// 获取到锁就持久占有,避免反复切换
while( !isStopped ){if( lock.isHeldByCurrentThread() ){// do some work}else{// try to acquire lock again. }SleepUtil.sleep(loopInterval, TimeUnit.MINUTES);
}

4、能用本地锁 不用全局锁。

  • 锁超时
1、合理设置锁的TTL,结合自己业务场景做取舍
例如,加锁之后执行大量数据的batch计算的场景。
如果锁TTL太长,那计算被异常中断(如机器重启)时,这个长TTL内是无法被其他节点/线程获取到执行权限的;但如果TTL设置太短,那可能还没等执行完成,锁就被意外抢走了。
2、注意watchDog机制
像Redisson之类的会有锁的watchdog,超过设置或默认的时间,锁就被偷偷释放了。
  • 释放锁
1、非必要情况下,避免强行释放锁,要检查锁的持有人是否是自己。
2、对于没有TTL的锁,要考虑极端情况下(进程被强制杀死、机器重启)的锁状态管理。否则意外一旦出现,锁就永远丢失了。

4 缓存问题

  • 缓存穿透问题
缓存和数据库都没有的数据,但被大量请求,导致DB压力过大。
常见的解决方式:对空值也进行缓存,但TTL设置相对较短。
  • 缓存击穿问题
一般是缓存的热点key发生过期失效,此时大量请求透过缓存 击中DB,导致DB压力过大。
常见解决方式:缓存查询miss时,设置个互斥锁,只允许一个request真实请求DB和重写缓存,避免大量请求涌入。
  • 缓存雪崩问题
缓存中的大量数据在较短的时间段内集中过期。一般发生在流量一波波来,缓存创建时间和TTL很接近。
常见解决方案:在TTL设置上不是一刀切,而是在一个合理范围内随机浮动,避免缓存集中失效。
  • 缓存的一致性
一般情况下,一致性要求不会非常严格。但如果需要强一致性保障时,要考虑缓存和DB之间的数据强一致性。
一种可能的方案:只在写DB时才写缓存,读DB操作不写缓存。DB和缓存的写操作要加锁,避免并发问题。具体流程如下:
当写DB请求发生时:
1、删除 缓存。此时读操作缓存会miss,读取到DB中的老值。
2、写入DB。此时读操作缓存会miss,读取到DB中的新值。
3、写入缓存。此时读操作缓存会 hit,读取到缓存中的新值(与DB新值一致)。
需要注意的是:
1、缓存针对数据库所有的数据记录,可能导致缓存空间占用高,实际利用率却不高。
2、如果某个缓存key 是热点,或者 流量比较大,尽管缓存“删除-重写入”间隔短,依然可能会引发 缓存击穿问题。
3、如果缓存写入失败,需要有相应的补偿机制再写入,且需关注 补偿写入与其他正常写入的冲突和时序问题。
  • 缓存命中率
这个本身不是问题,但命中率低说明缓存的设计或使用存在问题,需要重新设计。
  • 热点key问题
如果特定缓存节点CPU使用率远高于其他节点,说明可能存在热点key。这个时候需要合理对缓存key做拆分,将流量进一步打散。

5 失败处理问题

这类问题虽属于低级问题,但往往比较隐蔽。在异常发生时,选择相应处理action时,我们要头脑非常清醒。

  • 失败处理
可能的处理方式:
1、failover。失败立即重试。
2、failback。记录失败,后置处理。
3、failfast。直接失败,返回异常。
4、failsafe。忽略失败,继续流程。

这里不在于选择那种处理方式,而是要“头脑清醒”的结合自己场景需求做出选择。

  • 注意默认值
一些情况下,我们会初始化时设定一些默认值、默认状态等,对于这些情况要充分考虑异常发生时是否存在风险。
例如,在最开始时,代码里配置了当时的开城信息,但这个状态并没有跟业务操作流程打通,也就是没有办法做到及时更新。
那随着时间发展,开发了新的城市,那就可能产生问题。

6 switch配置问题

  • 分批推送的时间间隔
switch发布时,不同批次会有时间间隔,大部分场景下都可以容忍这个时间间隔。但个别情况下,可能引发诸如数据不一致等问题。
再使用switch时需要对这个问题做提前考虑,若不能容忍这种情况,那需要更换其他方案。
  • 内存值与持久值
switch的逻辑是这样:
1、switch会默认记录代码中的默认值。此时并不是 持久值。
2、当在代码中修改默认值时,switch平台也会显示代码默认值。此时也并不是 持久值。
3、只有在switch平台修改值并推送成功,swith平台会保存持久值。
4、switch保存持久值之后,不管代码修改默认值还是去掉 @AppSwitch 配置,持久值都是存在的。
如果你看到switch平台上展示了开关值,以为已经持久化,然后在代码里就把默认值删掉,此时也可能导致故障。
  • 代码重构注意事项

做代码结构重构时,如果没有指定switch的namespace,会导致你推送过的持久化开关失效,进而引发严重的线上故障。

关于应用级服务发现与接口级服务发现的区别和 dubbo 生态的解决方案,本文中不多赘述,可以参考刘军前辈写的文章文章《Dubbo 迈出云原生重要一步 应用级服务发现解析》
简单来说,应用级服务发现需要开发者关心接口之外还要关心应用名,注册中心的冗余信息较少;接口级服务发现开发者只需要引入接口名,但注册中心的冗余信息较多。

  • 合理使用,避免滥用
switch 提供了简单易用的配置化能力,但不要把应该正常编码要考虑和处理的问题,丢到switch上做开关。否则,最后开关一大堆,维护越发困难,就隐藏了风险。

7 重大风险评估和处置

针对一个需求开发,我们需要评估风险及我们的承受能力。主要目的是 预防重大故障的发生,而不是要预防所有Bug。

关于风险处置,也没有一个固定的标准。我建议是结合业务场景,评估风险概率和潜在问题的严重程度,最后来制定相应的解决方案。例如,如果发现有资损风险,那要采取一切手段把漏洞堵上;但如果只是小概率的漏掉钉钉通知,那增加相应的告警即可。

我们如何评估 重大风险呢?我建议分这么几个环节做评估:

1、梳理 关键的业务流。
2、梳理 每个业务流的关键环节。
3、梳理 每个关键环节的关键逻辑 和 关键上下游。
4、结合自己场景,假定 关键逻辑 和 关键上下游 出现极端问题。例如 网络挂掉、机器重启、高并发来临、缓存挂掉等。

这里需要强调一点,并非所有模块都需要假定非常极端的情况,要结合自己实际业务要求、历史风险等 来综合判断。

再举个例子:

假设,有一个用户资金转账系统,用户可以通过App进行跨行转账操作。
那这个系统就要考虑到 转账超时、转账失败等场景。同时还要考虑 转账超时 或 失败时,是fail-fast 好,还是 fail-over好?
此外,还需要考虑到 App端的用户交互设计,假如遭遇网络中断或超时,且用户看不到任何问题提示,那用户很可能再次发起转账尝试,最后转了两笔的钱。

这个评估过程看上去有点冗长,但其实对于了解自己系统和需求细节的人来讲,应该是很容易做到的。如果做不到那就只能加强细节的理解和学习了。

三 最后

以研发同学为中心,向内看:需持续提升防御性编码的意识和实操能力;向外看:外部环境需要尽可能提供与之匹配的环境。

例如,在面临有紧急DeadLine的需求时,防御性编码的执行完整度就会受到一定影响。

再次欢迎大家把自己的心得留言。

原文链接

本文为阿里云原创内容,未经允许不得转载。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/510926.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

消息队列Kafka「检索组件」上线

简介&#xff1a;本文对消息队列 Kafka「检索组件」进行详细介绍&#xff0c;首先通过对消息队列使用过程中的痛点问题进行介绍&#xff0c;然后针对痛点问题提出相应的解决办法&#xff0c;并对关键技术技术进行解读&#xff0c;旨在帮助大家对消息队列 Kafka「检索组件」的特…

从这些云原生企业身上,我看到了数字化创新者该有的样子

简介&#xff1a;未来的数字世界是什么样子&#xff1f;手握云原生地图的企业&#xff0c;又将如何颠覆数字化&#xff1f;带着这些疑问&#xff0c;我们采访了教育、金融、新零售、互娱、传媒等领域的多位大咖&#xff0c;他们是数字创新的引领者&#xff0c;也是用云原生技术…

云采销:赋能企业商机转化及营销全流程数字化

简介&#xff1a;介绍云采销-企采商产品的在企业业务中的应用场景和产品功能。 云采销&#xff1a;为企业提供“采购协同工具市场资源数据增值服务”的综合性解决方案。 其中“企采商营销管理工作台”为企业提供了B2B营销全域管理系统&#xff0c;包括商机挖掘、线上推广、销…

Hadoop 王者 Cloudera 新转型,定位为混合数据公司

作者 | 宋慧 出品 | CSDN 云计算 随着 AI 等技术兴起&#xff0c;对于数据的应用分析受到了越来越多的重视&#xff0c;数据赛道热度也持续火热。大数据时代为企业提供 Hadoop 服务的 Cloudera&#xff0c;也推出了新一代数据平台 CDP&#xff0c;并逐渐替代以往的大数据平台 …

大数据在线离线一体化解决方案最佳实践

简介&#xff1a; 本文重点介绍大数据产品集通用解决方案&#xff0c;即大数据在线计算离线计算一体化解决方案&#xff0c;并通过真实案例模拟来说明此通用解决方案在具体项目中是如何落地的。 概述 本方案重点要落地的业务是中央网信办网络安全应急指挥中心相关业务&#x…

基于MaxCompute的大数据安全方案

简介&#xff1a;随着法律的完善&#xff0c;数据安全&#xff0c;信息安全&#xff0c;网络安全&#xff0c;升级成国家安全&#xff0c;所以数据安全不管对用户&#xff0c;还是对公司也都会变的越来越重要。做为大数据云数仓解决方案的领导者&#xff0c;阿里云MaxCompute在…

数据仓库的分层架构与演进

简介&#xff1a;分层架构很容易在各种书籍和文档中去理解&#xff0c;但是把建模方法和分层架构放在一起就会出现很多困惑了。接下来&#xff0c;我会从数据研发与建模的角度&#xff0c;演进一下分层架构的设计原因与层次的意义。 分层架构很容易在各种书籍和文档中去理解&a…

数据治理之参考数据与主数据管理

简介&#xff1a;最近凑巧参与了一次某行业的业务共创会议&#xff0c;期间讨论到了主数据系统&#xff0c;还有我们该如何参与主数据系统建设的话题。说实话&#xff0c;我一直以为我不会有机会参与到主数据与参考数据系统的话题中去&#xff0c;所以&#xff0c;又去把DAMA的…

如何在云端重塑内容生产?来看这场虚拟人主持的发布会

简介&#xff1a;「智能媒体生产」产品全新升级 3月30日&#xff0c;阿里云视频云在线上举行了一场由虚拟人助力主持的「智能媒体生产」产品升级发布会&#xff0c;活动围绕产品能力的展现、视频生产流程的革新、高效生产背后的技术先进性&#xff0c;阐释了企业如何在云端重塑…

阿里开源自研工业级稀疏模型高性能训练框架 PAI-HybridBackend

简介&#xff1a;近年来&#xff0c;随着稀疏模型对算力日益增长的需求, CPU集群必须不断扩大集群规模来满足训练的时效需求&#xff0c;这同时也带来了不断上升的资源成本以及实验的调试成本。为了解决这一问题&#xff0c;阿里云机器学习PAI平台开源了稀疏模型高性能同步训练…

Serverless 遇到 FinOps,云成本问题有解了!

Key Takeaways&#xff1a;1. 尽管 Serverless 的迅猛发展吸引了广泛深入的关注&#xff0c;Serverless 函数总成本的事先估计仍缺乏有效的理论指导。本文基于 FunctionGraph 在 Serverless 领域的 FinOps 探索和实践&#xff0c;提出业界首个 Serverless 函数总成本估计模型。…

Apsara Stack 技术百科 | 联结良性生态,筑千行百业的数字基石

简介&#xff1a;作为现今IT领域最重要的课题&#xff1a;基础设施云化&#xff0c;离不开与伙伴的携手合作&#xff0c;如何让云上解决方案能充分释放价值的同时形成一个相互依存的自循环生态系统&#xff0c;混合云君来跟你聊聊~ 生态系统这个词在维基百科上的定义是&#xf…

用户留存建模实践

简介&#xff1a;在流量分析型产品的用户分析模块中&#xff0c;留存、互访、新老客构成等数据都是有效衡量用户粘性与促活召回的关键性指标&#xff1b;但是&#xff0c;我们发现在很多流量运营的业务场景中&#xff0c;留存分析建模都显著存在着设计和计算上的诸多问题。本文…

ACK One 构建应用系统的两地三中心容灾方案

简介&#xff1a;本文侧重介绍了通过 ACK One 的多集群应用分发功能&#xff0c;可以帮助企业管理多集群环境&#xff0c;通过多集群主控示例提供的统一的应用下发入口&#xff0c;实现应用的多集群分发&#xff0c;差异化配置&#xff0c;工作流管理等分发策略。结合 GTM 全局…

英特尔On技术创新峰会:助力开发者解决当前和未来的挑战

第二届英特尔On技术创新峰会于2022年9月27日在美国加利福尼亚州圣何塞市开幕。在本届峰会上&#xff0c;英特尔向齐聚一堂的软硬件开发者们分享了在构建以开放、选择和信任为原则的生态系统方面的最新进展——从推动开放标准以使“芯片系统”&#xff08;systems of chips&…

你不知道的 HTTPS 压测

简介&#xff1a;随着互联网安全规范的普及&#xff0c;使用 HTTPS 技术进行通信加密&#xff0c;实现网站和 APP 的可信访问&#xff0c;已经成为公认的安全标准。本文将介绍针对 HTTPS 协议做压力测试的关注点&#xff0c;以及使用 PTS 做 HTTPS 压测的技术优势和最佳实践。 …

数据湖—Delta Lake

简介&#xff1a;Delta Lake 是 DataBricks 公司开源的、用于构建湖仓架构的存储框架。能够支持 Spark&#xff0c;Flink&#xff0c;Hive&#xff0c;PrestoDB&#xff0c;Trino 等查询/计算引擎。作为一个开放格式的存储层&#xff0c;它在提供了批流一体的同时&#xff0c;为…

2022杭州云栖大会定档11月3日至5日:技术产品发布+超4万平科技展

9月28日消息&#xff0c;记者从云栖大会组委会获悉&#xff0c;2022杭州云栖大会将于11月3日至5日在杭州云栖小镇举办。今年云栖大会以“计算进化未来”为主题&#xff0c;在3天内设置两场主论坛&#xff0c;70多场数字技术、产业和生态分论坛&#xff0c;以及4万平米智能科技全…

阿里云RemoteShuffleService 新功能:AQE 和流控

简介&#xff1a;阿里云EMR 自2020年推出 Remote Shuffle Service(RSS)以来&#xff0c;帮助了诸多客户解决 Spark 作业的性能、稳定性问题&#xff0c;并使得存算分离架构得以实施。为了更方便大家使用和扩展&#xff0c;RSS 在2022年初开源(https://github.com/alibaba/Remot…

如何使用Delta Lake构建批流一体数据仓库

简介&#xff1a;Delta Lake是一个开源存储层&#xff0c;它为数据湖带来了可靠性。Delta Lake提供了ACID事务、可扩展的元数据处理&#xff0c;并统一了流式处理和批处理数据处理。Delta-Lake运行在现有数据湖之上&#xff0c;并且与Apache Spark API完全兼容。希望本篇能让大…