IT技术博客大学习 共学习 共进步

分布式程序设计早知道-关于分布式程序设计常见问题分析

孙豪杰的博客 2017-03-11 23:37:46 浏览 2,402 次

虽然系统越来越复杂,以及新分布式架构设计的思想普及,越来越多的系统采用了分布式的架构,特别是HTTP为交互方式的接口调用,移动端和PC端的并行对分布式架构带来了很大的推动。各式各样的服务接口,在处理业务流程之外有一些共性的问题,正视设计和解决这些问题,会大大提高程序的可用性,扩展性和可维护性。
以下总结是笔者工作中对于分布式设计问题的总结,具体内容如下:

1、日期格式

时间在生活中是一个容易忽视但是影响很大的一个因素,在古代,是否有自己的历法是评定一个统治者是否建立通知的一个重要的标志。应用程序中大量存在日期数据,比如数据的创建时间,更新时间,业务处理时间等。接口交互中关于日期传输需要考虑两个问题:格式和精度。

日期的格式主要有两种主要的方式:可读方式和不可读(不容易直接读)的方式。比如对于当前时间(笔者写此文章的这一刻),可以使用2017-03-01 20:30:30或者2017/3/1 20:30:30等其他的明确年月日时分秒各个元素可读格式表示;另外一种方式为以”1970-1-1 08:00:00″为基准的long型计数法。可读型的格式在调试,排查问题时方便易懂,但是在开发过程中可能需要做数据解析,转换为开发语言识别的类型用于日期的操作,增加了部分开发工作量。long型格式在执行效率上要高效,但是在开发调试过程中需要转换表示方式。虽然本人更加赞同long型的格式,但是如果按照可读型的方式返回,对于大多数应用不会有很大的区别。但是需要统一方式,系统中多个服务提供方对于日期格式的不统一会给客户端调用带来大量无效代码的开发和错误引入的可能性。

常用的计算机或者开发语言关于时间的最小单位为微秒,但大部分的业务使用秒级已经满足需要了,所以可以定义通用单位为秒,如果需要接口可以自定义可精确到微秒甚至更低。如果没有通用单位的定义,在以long型传输时会导致差之毫厘谬以千里的错误。

2、小数的传输

我们先来看一个简单的浮点数计算小程序,以java语言为例:

doublea = 1;
doubleb = 0.99;
System.out.println(a - b);

结果为:0.010000000000000009
计算机的世界只有0和1,现实中的十进制的整数、小数都会转换为二级制表示,在进行转换会导致精度丢失。
浮点数在计算机内部存储的格式转换过程如下:
小数点精度
大部分的编程语言都会有更高精度方式处理浮点数的运算,保证计算的精确性,比如Java语言的BigDecimal,我们使用BigDecimal再次测试1-0.99:

BigDecimal abd1 = newBigDecimal(a);
BigDecimal bbd1 = newBigDecimal(b);
System.out.println(abd1.subtract(bbd1));

结果为:
0.0100000000000000088817841970012523233890533447265625
说好的解决问题呢?
原来我们在已经定义了double类型初始化了a、b,当我们使用a、b来初始化BigDecimal对象时精度已经丢失,所以虽然采用了更高精度表示的BigDecimal类进行浮点数运算,仍然不能正确的获得差。
正确的方式为:

BigDecimal abd = newBigDecimal("1");
BigDecimal bbd = newBigDecimal("0.99");
System.out.println(abd.subtract(bbd));

结果为:0.01

因此我们在数据交互中,浮点数最好以字符串的方式进行传输,保证调用方在进行浮点数计算时可以采用不损失精度的方式进行初始化并运算。

3、结果值的格式

如果你服务提供方是否经常会听到这样的抱怨:结果值返回格式不统一,结果值的属性名不一样,属性的个数不一样,值表示的含义不一样,等等。如果具有通用意义的调用状态,错误码,错误信息提示,业务结果返回方式风格不统一,调用方在进行封装接口调用,错误重试,结果值解析等通用功能时,将不得不花更多的精力,以及牺牲程序可读性,扩展性。

推荐使用以下格式作为接口的返回值:
status: 接口调用状态,用于返回操作结果的状态 比如 200:成功 400:参数错误 500:服务器端错误
errorCode: 用于失败状态时的原因编码,用于客户端接口返回失败流程判断或者结果显示
message: 成功或者失败的消息提示,用于展示
data:用于表示返回接口业务数据的属性

分析一些知名公司的开发平台接口,存在把status和errorCode合成一个属性表示接口,但笔者认为分开表示更友好,因为调用结果状态是有限的,比如成功,参数错误,失败等,是一个业务无关,但是失败的原因(errorCode)是业务相关的,并且可以无限多,并且多个服务端的错误码存在共用的可能性,因此建议使用两个属性表示结果状态和错误编码。

对于返回的业务数据,建议把data作为Map格式,把结果值作为map的键值对存入,方便返回多个业务数据的情况。

4、幂等性

幂等性:指一次和多次请求某一个资源应该具有同样的副作用。幂等性是分布式系统设计中十分重要的概念,在分布式系统中网络抖动(不可预知的短期不可达到),业务超时等都可能导致调用方没有收到服务器端的返回值或者不是预期的返回值(比如:应该接收一个JSON字符串,但是返回了一个网络异常),为了保证系统的高可用性,重试是一个经常会采用的方法,因此服务器端可能会收到多次调用,保证多次调用具有相同的副作用是接口的重要属性。

接口调用

如上图所示以上5个步骤中任何一个流程出现问题都会导致客户端不能接收到预期的结果,可能执行重试,从步骤3开始服务器端的业务可能已经执行完毕,如果重试表示服务方会接收两次相同的调用。服务方识别出重复调用,并且当已经执行过业务逻辑,不再次执行重复并返回正确的结果就是幂等性。

如何实现幂等性呢?
1、业务天然符合幂等性,比如大部分的查询,无论何时调用不会对服务方数据有变更,因此天然具有幂等性。
2、基于入参的关键字段进行逻辑判断,比如在下单时扣减积分的场景,业务规定了每个订单只能扣减一次积分,因此在积分服务可以使用订单号作为关键字段做积分是否已经扣减的检查,如果已存在当前扣减积分的订单号表示已经执行过,直接返回结果,否则执行积分扣减并返回结果。
3、存在一些调用没有关键字段的情况,比如用户购买商品下单,无论是商品ID,商品数量,价格,会员ID都无法作为关键字段用于检查是否已执行,有人说可以使用入参是否完全一致作为判断条件,实际中这样是可以作为判断依据,但是理论上我们无法完全断定是用户的主动行为还是系统的异常重试。对于这种场景,引入一个业务无关的关键字段可以更好的解决问题,无论命名这个字段为requestId,ticketId或者其他,有如下要求:a、这个字段是由客户端生成的,b、全局唯一的(至少要一个可能会发生重试的时间范围或者空间范围内是唯一的),服务器端可以依据该字段作为是否重复调用的依据。

如何判断是否存在关键字段,也存在随着业务发展以前作为关键字段的数据不能作为判断依据了,因此在设计开发时所有的接口都添加requestId(假设我们采用了requestId这个命名),使用requestId作为幂等性的依据可以提高接口的可用性和重试的校验功能的重用性。

5、接口安全

作为接口提供方有时候会遇见一些场景:错误的入参,导致业务异常、错误提示,但是不能确定数据是哪里产生的;接口升级不知道需要通知哪些调用方,因为调用方都可以通过文档自行接入;部分调用方需要特殊业务处理,但是没有关键字段把它与其他调用方区分;数据统计缺少关键字段判断来源等。

因此入参中含有准确的表示调用方字段,并能够识别是否为调用方身份,可以提高系统的安全性,扩展性。
常见的方式有:
1、分配标识符appCode,每次调用传递该字段作为判断依据,优点:使用简单,缺点:但是appCode可以被复用,导致无法准确判断身份。
2、分配标识符appCode,并分配秘钥,采用对称加密的方式对入参签名,优点:使用方便,相比非对称加密效率高,缺点:秘钥至少需要双方甚至多方同时保存,安全性和准确性降低。
3、分配标识符appCode,并分配非对称加密的密钥对,对入参使用非对称加密的方式签名。优点:安全性和准确性高。缺点:效率低。

对比以上常用的方式的优劣,建议采用在性能不敏感(非对称和对称加密在对入参进行Hash后签名的实现方式时效率差别不大)的情况下尽量采用非对称加密,因为在多对多的交互场景中,非对称加密比对称加密的签名方式更高的安全性。
对称加密在多对多交互的场景中交互如下:
对称加密

上图所示:公用密码会导致密码扩散,非公用密码导致调用方保存多套密码。

采用非对称加密多对多交互方式:

非对称加密

上图所示:CS秘钥对分别在调用方和服务方保存,保证了安全性和准确性。

6、数据字典

分布式架构使系统中不同的模块可以在不同的团队开发维护,因此属性含义和命名的一致性的要求急剧上升,无数次的参数命名转换就是命名不一致的墓碑。

命名不一致主要包含2个方面:
1、含义不一致,曾经参与开发的一个系统,其中Product表示了至少两种差别很大的含义。a、表示交易时用户购买的商品,b、交易完成后对外提供服务的抽象命名,同时多次交易可能修改同一个服务,系统中关于这个数据的转换满天飞。
2、命名不一致,因为各种开发语言天然支持字母类命名的,因此常用的类名,方法名,变量等都会以英文命名,同时程序员英文水平的参差不齐,所以导致同样的含义命名各式各样,比如积分,有Point,也有Score,还有命名为integral。

如果没有很好沟通和协商,就会导致调用方各种转换,增加大量无用属性转换代码,并且容易引入错误。因此一个符合团队条件的数据字典管理(wiki,文档,git,应用程序等)可以提供开发的效率和可用性。

以上几点是笔者在项目开发时的心得体会,若能在你设计开发分布式系统时带来些许帮助,就深感欣慰。当然分布式设计开发不仅仅以上几个问题可以探讨,比如事务,交互过程的超时处理,重试机制等,限于笔者水平以及关于通用问题的判断,本文没有涉及,见谅。
如有错误也请不吝赐教。

建议继续学习

  1. 分布式缓存系统 Memcached 入门 (阅读 16,042)
  2. Zookeeper工作原理 (阅读 11,942)
  3. GFS, HDFS, Blob File System架构对比 (阅读 10,342)
  4. Zookeeper研究和应用 (阅读 9,341)
  5. 一致性哈希算法及其在分布式系统中的应用 (阅读 9,043)
  6. 分布式日志系统scribe使用手记 (阅读 8,843)
  7. 分布式哈希和一致性哈希 (阅读 8,665)
  8. HBase技术介绍 (阅读 7,942)
  9. 分布式系统的事务处理 (阅读 7,244)
  10. Memcache分布式部署方案 (阅读 6,666)