对于产品,一般来讲,从单模块,到负载均衡的多模块,最后到有服务治理的规模化集群(例如微服务),逐步发展和演进。本文并不打算涉及框架或者架构,也不讲什么大道理,仅从代码编写的角度,看看开发人员需要注意什么。
单模块并不是指单体方式,根据功能进行模块划分,每个模块在生产环境中是单模块运行(主备方式)。单模块阶段开发人员仍是有要求,在我的实践中,从code review看,经验不足的开发人员会存在以下的疏忽:
一、开发版本、测试版本和生产版本不是同一个版本。What?怎么回事,就是开发人员将配置数据、业务数据(包括log所在路径)都统统打包在一起。
我们以 tomcat 的 war 包为例,一般来讲,开发人员不会将诸如数据库的ip地址、密码、服务提供者的url写入到程序代码中,但是会写入诸如web.xml的配置中,一起打包。这就需要专门为测试环境、生产环境修改配置,重新打包,就有了测试版本,生产版本。但实际上,生产环境的ip地址,账号,密码,开发人员并不需要(也没必要或者不应该)知道,而且一旦生产环境发生变化,都需要返工打包,运维会崩溃的。
有些开发人员会认为,安装了 war 包之后,tomcat 将之解压为文件夹,可以手动修改里面的配置文件。这想法需要扼杀在摇篮当中:
我们看看linux是如何组织软件包的,程序在/var/lib或者/usr/lib,配置在/etc,log在/var/log下面。这是将业务逻辑(程序)、配置、业务完全独立开来。简单讲就是:代码(逻辑)归代码(逻辑),数据归数据。如果我们混在一起:开发人员开发环境或测试环境的数据,log会污染到生产环境,而生产环境中的业务数据,log很容易因版本升级中出现数据丢失,哪怕是丢失了log,在生产环境中都是运维事故。要养成将逻辑、配置、数据分离的开发好习惯。这种分离,在集群编排部署中尤为重要。
二、缺乏对同一资源并发处理的关注。不要以为单模块就没有并发处理,现在哪个程序不是多线程的。对于刚入行的程序员,比较容易出现下面的问题:
三、优雅关闭的问题。优雅关闭是在程序结束时,释放程序占用的所有资源,保证正在的处理业务,能够处理完,而不能是处理了一半这种不确定的状态,可以选择拒绝业务,更多的情况是完成业务。
我看过升级时,简单粗暴的直接将tomcat给 kill 掉的,这种方式,如果数据库的事务没有收到commit,这个事务会挂起;又例如 tcp的keepalive在阿土中缺省是2小时,而数据库的连接数是有上限的。还有如果业务执行到一半,可能导致该用户后续业务状态异常而挂起,由于log也被突然中断,出现了问题很难排除。
四、缺乏对异常流程的关注。用户的输入,内部服务提供方或者第三方api的调用,都可能存在意外,开发人员对本模块外的是不信任的,开发世界是性本恶。最简单的,万一web api不是响应200,而是4xx,5xx,你怎么处理。我们在原型中可以不考虑,但是在生产环境中,程序必须以警惕的眼光看待外面的世界。
五、业务逻辑抽象很重要。这是区别你会写几行代码,还是会开发。良好的业务抽象,当业务需求不断增加时,我们可以游刃有余,说不定就只是原来基础抽象的组合,代码具有良好的业务扩展能力。如果不注意抽象,说白了就是我们程序内部的逻辑设计,来一个新的需求,吭呲吭呲地写一堆代码,将其堆叠在原来的代码之上,几个需求后,就是一堆草,代码没法看了。
六、接口名字要清晰表达其准确功能。例如选择接口的名字叫deleteUser,resetUserPolicy,或者resetUserPolicyAndClearUserStock。要给接口函数加上注释。我们不是交一次作业,代码要维护要发展,生命期长着呢,有时开发人员自己都不太记得了。应有写文档写注释的习惯,明确调用的影响。
在某个案例中,开发人员只是想重置用户的某个属性,结果连带将用户的持有物品清单也一并清空。只要是人都会犯错,但是有很多可以通过规范化的开发流程来避免,在这个案例中:
从单模块到多模块,最基础的,就是 AKF 的 X 轴扩展。不要认为 x 轴扩展是自然天成,多部署几个 copy 就可以了。X轴扩展也是有条件的。我们举个例子,某个web网站,用户登录进去后可以进行数据的增删改查,具体业务是什么不重要,以 J2EE spring 为例,是否可以简单部署 n 个模块,在前面加上负载均衡器(LB)就OK了?
没那么简单,当用户登录成功,会分配一个 session 给该用户(浏览器),在后续的访问中,浏览器的 http 请求中会通过 cookie 携带 session id,服务器由此判断用户的合法身份。如果使用最常见的轮询负载均衡,模块 A 收到用户的登录请求,分配了 session,并保存在内存中,用户下一个请求,被路由到模块 B,模块 B 的内存中并没有这个session的ID,由此判断用户无效,重定向到登录页面中。如此,来来回回,这个可怜的用户就在不断地登录成功和重新要求登录中挣扎。
因此,如果不做任何的处理,直接将模块 copy n 份,可能会导致整个业务出现问题。例子的问题就是模块 B 无法获得模块 A 内存中的session信息,要处理,可以沿两个思路:
先看看消灭问题,就是不让问题出现。例子只要确保相同用户,也即相同 session id 的 http 请求能够路由到同一模块即可。这种方式,对代码没有要求,但是对负载均衡器有要求,要求将相同的 session id 的 http 请求路由到同一个上游模块。方式有很多,例如根据 session id 的哈希值取模,也可以作为 proxy,在 session-id 的结尾加上一个上游服务器的标记,等下次 http 到来时,可以根据 session id 得到它以往是哪个上游模块处理的。这种方式虽然对代码没有要求,但是对运维和部署有要求,开发人员应明确指出,以此要求负载均衡器。例如,如果使用 nginx,是否该用openresty,通过 lua 来自定义路由。
如果采用解决问题的方式,就是让 B 模块也能获取 A 模块分配的 session id,也就是 A 模块不能将 session 数据只放在自己的内存中,由于模块可能部署在不同的机器中,因此也不能通过本机共享内存的方式。如果你想到存放在数据库,那么思路方向对了,我们确实要找一个各模块都能访问的地方来存放 session 数据,但是这个不能采用持久化的存储,如数据库。很简单,这就不是一个持久化的数据,这是临时的,会变化的数据,不需要也不应该存放在数据库。如果放在数据库,会加重数据库的读写压力。这时高速缓存,例如 redis,memcached 的就是比较合适的选择。由于引入了高速缓存,系统的架构就出现了变化,对代码就有要求,本地内存读写变为高速缓存的读写。
由此可见,横向的 x 轴的扩展绝不是简单将模块copy几份,前面加个负载均衡就可以。有了多模块,跟着会有很多新的问题,例如服务发现、全链条跟踪等,这些就是微服务的事情了。