伦敦 伦敦00:00:00 纽约 纽约00:00:00 东京 东京00:00:00 北京 北京00:00:00

400-668-6666

分层编码

当前位置:主页 > 分层编码 >
分层编码

Dubbo的一些编码约定和设计原则

  最近一直担心 Dubbo 分布式服务框架后续如果维护人员增多或变更会出现质量的下降 我在想有没有什么是需要大家共同遵守的根据平时写代码时的一习惯总结了一下在写代码过程中尤其是框架代码要时刻牢记的细节。可能下面要讲的这些大家都会觉得很简单很基础但要做到时刻牢记。在每一行代码中都考虑这些因素是需要很大耐心的 大家经常说魔鬼在细节中确实如此。

  这是我最不喜欢看到的异常尤其在核心框架中我更愿看到信息详细的参数不合法异常。这也是一个健状的程序开发人员在写每一行代码都应在潜意识中防止的异常。基本上要能确保一次写完的代码在不测试的情况都不会出现这两个异常才算合格。

  对于框架的开发人员对线程安全性和可见性的深入理解是最基本的要求。需要开发人员在写每一行代码时都应在潜意识中确保其正确性。因为这种代码在小并发下做功能测试时会显得很正常。但在高并发下就会出现莫明其妙的问题而且场景很难重现极难排查。

  尽早失败也应该成为潜意识在有传入参数和状态变化时均在入口处全部断言。一个不合法的值和状态在第一时间就应报错而不是等到要用时才报错。因为等到要用时可能前面已经修改其它相关状态而在程序中很少有人去处理回滚逻辑。这样报错后其实内部状态可能已经混乱极易在一个隐蔽分支上引发程序不可恢复。

  这里的可靠是狭义的指是否会抛出异常或引起状态不一致比如写入一个线程安全的 Map可以认为是可靠的而写入数据库等可以认为是不可靠的。开发人员必须在写每一行代码时都注意它的可靠性与否在代码中尽量划分开并对失败做异常处理并为容错自我保护自动恢复或切换等补偿逻辑提供清晰的切入点保证后续增加的代码不至于放错位置而导致原先的容错处理陷入混乱。

  如果一个类可以成为不变类(Immutable Class)就优先将它设计成不变类。不变类有天然的并发共享优势减少同步或复制而且可以有效帮忙分析线程安全的范围。就算是可变类对于从构造函数传入的引用在类中持有时最好将字段 final以免被中途误修改引用。不要以为这个字段是私有的这个类的代码都是我自己写的不会出现对这个字段的重新赋值。要考虑的一个因素是这个代码可能被其他人修改他不知道你的这个弱约定final 就是一个不变契约。

  前面不停的提到代码被其他人修改这也开发人员要随时紧记的。这个其他人包括未来的自己你要总想着这个代码可能会有人去改它。我应该给修改的人一点什么提示让他知道我现在的设计意图而不要在程序里面加潜规则或埋一些容易忽视的雷比如你用 null 表示不可用size 等于 0 表示黑名单这就是一个雷下一个修改者包括你自己都不会记得有这样的约定可能后面为了改某个其它 BUG不小心改到了这里直接引爆故障。对于这个例子一个原则就是永远不要区分 null 引用和 empty 值。

  这里的可测性主要指 Mock 的容易程度和测试的隔离性。至于测试的自动性可重复性非偶然性无序性完备性(全覆盖)轻量性(可快速执行)一般开发人员加上 JUnit 等工具的辅助基本都能做到也能理解它的好处只是工作量问题。这里要特别强调的是测试用例的单一性(只测目标类本身)和隔离性(不传染失败)。现在的测试代码过于强调完备性大量重复交叉测试看起来没啥坏处但测试代码越多维护代价越高。经常出现的问题是修改一行代码或加一个判断条件引起 100 多个测试用例不通过。时间一紧谁有这个闲功夫去改这么多形态各异的测试用例久而久之这个测试代码就已经不能真实反应代码现在的状况很多时候会被迫绕过。最好的情况是修改一行代码有且只有一行测试代码不通过。如果修改了代码而测试用例还能通过那也不行表示测试没有覆盖到。另外可 Mock 性是隔离的基础把间接依赖的逻辑屏蔽掉。可 Mock 性的一个最大的杀手就是静态方法尽量少用。

  最近给团队新人讲了一些设计上的常识可能会对其它的新人也有些帮助把暂时想到的几条先记在这里。

  什么是会线f;就是一次交互过程。会话中重要的概念是上下文什么是上下文比如我们说“老地方见”这里的“老地方”就是上下文信息。为什么说“老地方”对方会知道因为我们前面定义了“老地方”的具体内容。所以说上下文通常持有交互过程中的状态变量等。会话对象通常较轻每次请求都重新创建实例请求结束后销毁。简而言之把元信息交由实体域持有把一次请求中的临时状态由会线c;由服务域贯穿整个过程。

  这里先要讲一个事件和上面的区别是干预过程的它是过程的一部分是基于过程行为的而事件是基于状态数据的任何行为改变的相同状态对事件应该是一致的。事件通常是事后通知是一个 Callback 接口方法名通常是过去式的比如 onChanged()。比如远程调用框架当网络断开或连上应该发出一个事件当出现错误也可以考虑发出一个事件这样外围应用就有可能观察到框架内部的变化做相应适应。

  比如远程调用框架它的协议是可以替换的。如果只提供一个总的扩展接口当然可以做到切换协议但协议支持是可以细分为底层通讯序列化动态代理方式等等。如果将接口拆细正交分解会更便于扩展者复用已有逻辑而只是替换某部分实现策略。当然这个分解的粒度需要把握好。

  大凡发展的比较好的框架都遵守微核的理念。Eclipse 的微核是 OSGi Spring 的微核是 BeanFactoryMaven 的微核是 Plexus。通常核心是不应该带有功能性的而是一个生命周期和集成容器这样各功能可以通过相同的方式交互及扩展并且任何功能都可以被替换。如果做不到微核至少要平等对待第三方即原作者能实现的功能扩展者应该可以通过扩展的方式全部做到。原作者要把自己也当作扩展者这样才能保证框架的可持续性及由内向外的稳定性。

  因为使用环境的不确定因素很多框架总会有一些配置一般都会到 classpath 直扫某个指定名称的配置或者启动时允许指定配置路径。做为一个通用框架应该做到凡是能配置文件做的一定要能通过编程方式进行否则当使用者需要将你的框架与另一个框架集成时就会带来很多不必要的麻烦。

  我们平台的产品越来越多产品的功能也越来越多。平台的产品为了适应各 BU 和部门以及产品线c;势必会将很多不相干的功能凑在一起客户可以选择性的使用。为了兼容更多的需求每个产品每个框架都在不停的扩展而我们经常会选择一些扩展的扩展方式也就是将新旧功能扩展成一个通用实现。我想讨论是有些情况下也可以考虑增量式的扩展方式也就是保留原功能的简单性新功能独立实现。我最近一直做分布式服务框架的开发就拿我们项目中的问题开涮吧。

  Dubbo 现在的设计是完全无侵入也就是使用者只依赖于配置契约。经过多个版本的发展为了满足各种需求场景配置越来越多。为了保持兼容配置只增不减里面潜伏着各种风格约定规则。新版本也将配置做了一次调整去掉了 dubbo.properties改为全 spring 配置。将想到的一些记在这备忘。

  而描述配置通常信息比较多甚至有层次关系用 xml 配置会比较方便因为树结构的配置表现力更强。如果非常复杂也可以考自定义 DSL 做为配置。有时候这类配置也可以用 Annotation 代替 因为这些配置和业务逻辑相关放在代码里也是合理的。

  另外扩展配置可能不尽相同。如果只是策略接口实现类替换可以考虑 properties 等结构。如果有复杂的生命周期管理可能需要 XML 等配置。有时候扩展会通过注册接口的方式提供。

  配置的可编程性是非常必要的不管你以何种方式加载配置文件都应该提供一个编程的配置方式允许用户不使用配置文件直接用代码完成配置过程。因为一个产品尤其是组件类产品通常需要和其它产品协作使用当用户集成你的产品时可能需要适配配置方式。

  配置的缺省值通常是设置一个常规环境的合理值这样可以减少用户的配置量。通常建议以线上环境为参考值开发环境可以通过修改配置适应。缺省值的设置最好在最外层的配置加载就做处理。程序底层如果发现配置不正确就应该直接报错容错在最外层做。如果在程序底层使用时发现配置值不合理就填一个缺省值很容易掩盖表面问题而引发更深层次的问题。并且配置的中间传递层很可能并不知道底层使用了一个缺省值一些中间的检测条件就可能失效。Dubbo 就出现过这样的问题中间层用“地址”做为缓存 Key 而底层给“地址”加了一个缺省端口号导致不加端口号的“地址”和加了缺省端口的“地址”并没有使用相同的缓存。

  配置总会隐含一些风格或潜规则应尽可能保持其一致性。比如很多功能都有开关然后有一个配置值

  不管选哪种方式所有配置项都应保持同一风格Dubbo 选的是第二种。相似的还有超时时间重试时间定时器间隔时间。如果一个单位是秒另一个单位是毫秒(C3P0的配置项就是这样)配置人员会疯掉。

  配置也存在“重复代码”也存在“泛化与精化”的问题。比如Dubbo 的超时时间设置每个服务每个方法都应该可以设置超时时间。但很多服务不关心超时如果要求每个方法都配置是不现实的。所以 Dubbo 采用了方法超时继承服务超时服务超时再继承缺省超时没配置时一层层向上查找。

  另外Dubbo 旧版本所有的超时时间重试次数负载均衡策略等都只能在服务消费方配置。但实际使用过程中发现服务提供方比消费方更清楚但这些配置项是在消费方执行时才用到的。新版本就加入了在服务提供方也能配这些参数通过注册中心传递到消费方 做为参考值如果消费方没有配置就以提供方的配置为准相当于消费方继承了提供方的建议配置值。而注册中心在传递配置时也可以在中途修改配置这样就达到了治理的目的继承关系相当于服务消费者 -- 注册中心 -- 服务提供者

  向前兼容很好办你只要保证配置只增不减就基本上能保证向前兼容。但向后兼容也是要注意的要为后续加入新的配置项做好准备。如果配置出现一个特殊配置就应该为这个“特殊”情况约定一个兼容规则因为这个特殊情况很有可能在以后还会发生。比如有一个配置文件是保存“服务地址”映射关系的其中有一行特殊保存的是“注册中心地址”。现在程序加载时约定“注册中心”这个Key是特殊的做特别处理其它的都是“服务”。然而新版本发现要加一项“监控中心地址”这时旧版本的程序会把“监控中心”做为“服务”处理因为旧代码是不能改的兼容性就很会很麻烦。如果先前约定“特殊标识XXX”为特殊处理后续就会方便很多。

  Dubbo 作为远程服务暴露、调用和治理的解决方案是应用运转的经络其本身实现健壮性的重要程度是不言而喻的。

  日志是发现问题、查看问题一个最常用的手段。日志质量往往被忽视没有日志使用上的明确约定。重视 Log 的使用提高 Log 的信息浓度。日志过多、过于混乱会导致有用的信息被淹没。

  有了这样的约定监管系统发现日志文件的中出现 ERROR 字串就报警又尽量减少了发生。过多的报警会让人疲倦使人对报警失去警惕性使 ERROR 日志失去意义。再辅以人工定期查看 WARN 级别信息以评估系统的“亚健康”程度。

  出问题时的现场信息即排查问题要用到的信息。如服务调用失败时要给出使用 Dubbo 的版本、服务提供者的 IP、使用的是哪个注册中心调用的是哪个服务、哪个方法等等。这些信息如果不给出那么事后人工收集的问题过后现场可能已经不能复原加大排查问题的难度。

  如果可能给出问题的原因和解决方法。这让维护和问题解决变得简单而不是寻求精通者往往是实现者的帮助。

  同一个或是一类异常日志连续出现几十遍的情况还是常常能看到的。人眼很容易漏掉淹没在其中不一样的重要日志信息。要尽量避免这种情况。在可以预见会出现的情况有必要加一些逻辑来避免。

  如为一个问题准备一个标志出问题后打日志后设置标志避免重复打日志。问题恢复后清除标志。

  虽然有点麻烦但是这样做保证日志信息浓度让监控更有效。

  资源是有限的CPU、内存、IO 等等。不要因为外部的请求、数据不受限的而崩溃。

  Server 端用于处理请求的 ExectorService 设置上限。ExecutorService 的任务等待队列使用有限队列避免资源耗尽。当任务等待队列饱和时选择一个合适的饱和策略。这样保证平滑劣化。

  在 Dubbo 中饱和策略是丢弃数据等待结果也只是请求的超时。

  达到饱和时说明已经达到服务提供方的负荷上限要在饱和策略的操作中日志记录这个问题以发出监控警报。记得注意不要重复多次记录哦。注意缺省的饱和策略不会有这些附加的操作。根据警报的频率已经决定扩容调整等等避免系统问题被忽略。

  如果确保进入集合的元素是可控的且是足够少则可以放心使用。这是大部分的情况。如果不能保证则使用有有界的集合。当到达界限时选择一个合适的丢弃策略。

  目前服务注册中心使用了数据库来保存服务提供者和消费者的信息。注册中心集群不同注册中心也通过数据库来之间同步数据以感知其它注册中心上提供者。注册中心会内存中保证一份提供者和消费者数据数据库不可用时注册中心独立对外正常运转只是拿不到其它注册中心的数据。当数据库恢复时重试逻辑会内存中修改的数据写回数据库并拿到数据库中新数据。

  服务消息者从注册中心拿到提供者列表后会保存提供者列表到内存和磁盘文件中。这样注册中心宕后消费者可以正常运转甚至可以在注册中心宕机过程中重启消费者。消费者启动时发现注册中心不可用会读取保存在磁盘文件中提供者列表。重试逻辑保证注册中心恢复后更新信息。

  注册中心会定时更新数据库一条记录的时间戳这样集群中其它的注册中心感知它是存活。过期注册中心和它的相关数据 会被清除。数据库正常时这个机制运行良好。但是数据库负荷高时其上的每个操作都会很慢。这就出现

  A 注册中心认为 B 过期删除 B 的数据。 B 发现自己的数据没有了重新写入自己的数据的反复操作。这些反复的操作又加重了数据库的负荷恶化问题。

  当 B 发现自己数据被删除时写入失败选择等待这段时间再重试。重试时间可以选择指数级增长如第一次等 1 分钟第二次 10 分钟、第三次 100 分钟。

  当一个注册中心停机时其它的 Client 会同时接收事件而去重连另一个注册中心。Client 数量相对比较多会对注册中心造成冲击。避免方法可以是 Client 重连时随机延时 3 分钟把重连分散开。

  最近有点痴呆因为解决了太多的痴呆问题。服务框架实施面超来超广已有 50 多个项目在使用每天都要去帮应用查问题来来回回发现大部分都是配置错误或者重复的文件或类或者网络不通等所以准备在新版本中加入防痴呆设计。估且这么叫吧可能很简单但对排错速度还是有点帮助希望能抛砖引玉也希望大家多给力想出更多的防范措施共享出来。

  配置文件加载错也是经常碰到的问题。用户通常会和你说“我配置的很正确啊不信我发给你看下但就是报错”。然后查一圈下来原来他发过来的配置根本没加载平台很多产品都会在 classpath 下放一个约定的配置如果项目中有多个通常会取JVM加载的第一个为了不被这么低级的问题折腾和上面的重复jar包一样在配置加载的地方加上

  必填配置估计大家都会检查因为没有的线c;根本没法运行。但对一些可选参数也应该做一些检查比如服务框架允许通过注册中心关联服务消费者和服务提供者也允许直接配置服务提供者地址点对点直连这时候注册中心地址是可选的但如果没有配点对点直连配置注册中心地址就一定要配这时候也要做相应检查。

  每次应用一出错应用的开发或测试就会把出错信息发过来询问原因这时候我都会问一大堆套线c;用的哪个版本呀是生产环境还是开发测试环境哪个注册中心呀哪个项目中的哪台机器呀哪个服务? 累啊最主要的是有些开发或测试人员根本分不清没办法只好提供上门服务浪费的时间可不是浮云所以日志中最好把需要的环境信息一并打进去最好给日志输出做个包装统一处理掉免得忘了。包装Logger接口如

  每次线上环境一出问题大家就慌了通常最直接的办法回滚重启以减少故障时间这样现场就被破坏了要想事后查问题就麻烦了有些问题必须在线上的大压力下才会发生线下测试环境很难重现不太可能让开发或 Appops 在重启前先手工将出错现场所有数据备份一下所以最好在 kill 脚本之前调用 dump进行自动备份这样就不会有人为疏忽。dump脚本示例

  随着服务化的推广网站对Dubbo服务框架的需求逐渐增多Dubbo 的现有开发人员能实现的需求有限很多需求都被 delay而网站的同学也希望参与进来加上领域的推动所以平台计划将部分项目对公司内部开放让大家一起来实现Dubbo 为试点项目之一。

  这里面虽然有部分扩展接口但并不能很好的协作而且扩展点的加载和配置都没有统一处理所以下面对它进行重构。

  由一个插件生命周期管理容器构成微核心核心不包括任何功能这样可以确保所有功能都能被替换并且框架作者能做到的功能扩展者也一定要能做到以保证平等对待第三方所以框架自身的功能也要用插件的方式实现不能有任何硬编码。

  保持尽可能少的概念有助于理解对于开放的系统尤其重要。另外各接口都使用一致的概念模型能相互指引并减少模型转换

  但它们的作用是一样的只是一个在客户端一个在服务器端却采用了不一样的模型类。

  泛化式扩展指将扩展点逐渐抽象取所有功能并集新加功能总是套入并扩充旧功能的概念。

  组合式扩展指将扩展点正交分解取所有功能交集新加功能总是基于旧功能之上实现。


点击次数:  更新时间:2020-10-02 23:05   【打印此页】  【关闭
http://gentecilla.com/fencengbianma/106/