什么是运维眼中可部署的软件架构

in 互联网技术 with 0 comment  访问: 4,039 次

在之前的文章优秀的软件或架构应具有哪些特性中从操作性、一致性和维护性介绍了一个优秀的软件架构应该具有的特点,今天谈一谈操作性分类下的可操作性。

可操作性在日常研发过程中,可能是比较容易忽略的软件非功能性的内容,因为大多数开发都在为业务和KPI服务,即使想到了这点,也在开发的过程中容易比较丢弃,因为不管是大公司还是创业公司,大多数开发者都在为业务、项目疲于奔命,有些东西想做好,但是永远没有时间,当前任务都完成不了,当然这种非功能性的内容容易被忽略掉;当然还有些是架构师不作为的原因。不管是时间问题、还是环境问题,都会造成开发者忽略对可部署特性的考虑。

软件工程我觉得是需要有工匠精神的,不管是谁,我想也不可能一开始就把软件设计的很好,总汇遗漏点什么,在不断的完善和优化软件,软件的成长就像一个人,通常软件的生命周期会经历类似幼年 --> 青年 --> 成年的这么一个过程。但是在商业化道路浓厚的情况下,这种精神经不起考验,就像老罗一样,有工匠精神又怎样,在商业化道路的逼迫下,还是失败了,造就了网红直播一哥的道路,今天好像有老罗抖音直播。

当你在疲于奔命写一些业务逻辑的时候,作为架构师or有自我认知的开发者,你应该不止步于开发完成,上线了就不管了,你应该在时间允许的情况下,继续优化你的项目,多从一些其他角度来提升架构的服务能力,比如我们常说的:可部署、可运维、高可用、容灾、稳定性等。

我们回归正题哦,什么是运维眼中的可部署的软件架构,可部署你也可以立即为容易部署或者叫做轻松部署,那要做到如此,需要有哪些方面的考虑。

依赖越少越好

更好的依赖,意味这你开发交付到测试、运维手里的软件,在部署层面越简单。依赖环境、依赖各种库、各种版本,就意味着要依赖别人去做这件事情,但是别人是否可信是要经得起推敲才行,所以较多的依赖,就会增加你软件的风险点。

如下所示,是一个传统LAMP架构中,PHP依赖的模块:

[PHP Modules]
bcmath
bz2
dom
ereg
exif
fileinfo
filter
ftp
gd
gettext
mhash
mysql
mysqli
mysqlnd
openssl
pcntl
pcre
PDO
pdo_mysql
pdo_sqlite
zip
zlib
......................

对于这类程序,当你问开发的时候,这些模块哪些有用,哪些是否可以去掉,回答基本都是都有用,能去掉的很少,那作为运维基本要吐血了,更可怕的是,这些依赖你还要找一个准环境挨个去看版本,是否版本不同程序运行是否兼容也不知道。

所以从易部署的角度来看,首先要做到的是,整理一份你程序的依赖关系和版本说明,别因为这个消息的传递不到位,运维白费力,虽然按照要求安装了要求的依赖模块,但是因为版本不对的原因,造成重复工作的过程,这样对整体的研发交付过程时间的浪费是极其多的,因为要不断实践、测试、回锅来过,沟通确认。

第二点就是减少对系统和库的依赖,我们先抛开CPU架构层面(一般X86、ARM、MIPS)不说,就说在常规的X86CPU架构之上。在Server端,一个软件的运行可能只能够在Centos7下面运行,换个Centos6或者Ubuntu就不能够运行了,这种情况就得学习一下Google的做法,Google对于C/C++和Golang等语言的程序都会使用静态编译的做法,这样就是为了减少依赖和减少动态库的版本冲突,而不是交付一个需要运维人员现编译和现解决依赖库问题的软件版本。

这点对于有Agent架构的程序来说尤其重要,不管是日志采集的Agent还是监控的Agent,基本上都要面临多系统和多CPU架构环境的适配,通常在不同的机器or环境(测试、预发布、生产)都各有差异,所以会有多种情况出现,如果少了个库、命令、包等依赖,那问题就显得比较明显了。

如今解决环境依赖无非最典型的利器就是利用Docker了,因为只要你的操作系统能够部署上Docker Deamon,你build 一个Docker image,那可以多处跑,容器技术其实出现比较早,最早出现的是LXC,是一种基于容器的操作系统层级的虚拟化技术,但是直到Docker Image的出现,Docker完全火起来了,因为在没有Image的情况下,你的所有流水线Build、Ship、Run就没有意义了。

除了Docker Image的解决方案,有另外一种解决依赖的手段就是利用CloudFoundry Buildpack机制。在Heroku和CloudFoundry上可以看到,Buildpack可以把用户代码编译之后,和依赖一起打包,比如Java Web程序,Buildpack会先把源码编译打包成War,然后和Tomcat、JDK一起,打成一个包,称为Droplet,然后环境部署的时候直接分发Droplet就好了。

当然还有就手动编译的方式,那就是梳理好依赖关系和兼容关系,所有的构建都进行编译操作,静态编译不用管,直接归档目标即可,动态编译的话,可以利用ldd -vreadlink命令结合写个打包归档动态链接库的工具,最后在程序启动的脚本里面添加LD_LIBRARY_PATH运行路径。

自动化配置

自动化配置就是减少人工配置项,尽可能的做到Zero Configuration,如果没法做到自动化配置,那就尽可能的让相同实例的配置一致,可能运维都比较烦因为配置不一致导致的问题。

自动探测服务器运行环境(但是要有一些规范),自动配置线程数、根据服务器内存的不同自动设置Java JVM参数等,都是典型的自动化配置的表现方式,不过这里如果一台服务器运行多服务、多实例的场景,就相对比较负责,就需要有些规范和策略。

如果是容器环境,就要注意容器的隔离性,因为容器的隔离性较差,获取到的信息是实际物理宿主机的配置,如果根据这些信息设置一些配置,比如JVM内存和线程数相关的参数,很有可能造成OOM,当然你可以通过API的方式从cAdvisor获取容器的CPU、内存、和磁盘网络等配置信息。

其次就是根据不同的运行环境来配置不同的配置,通常我们在软件上线的过程中,一般要经历 测试环境(QA) --> 预生产环境(Beta) --> 生产环境(Prod),而且不同的环境机器配置和机器数量也是不一致的,如果都一致化了,那就是成本问题。如果有中心化配置中心的话,比如携程的Apollo,根据不同的环境标记,应用程序自动从配置中心拉取(或推送)对应环境的配置信息。在没有配置中心的情况下,我们预想要做一些规范,比如主机名的密码规范, 产品-机房位置-环境-服务分类(log-ali-qa-nginx1, log-ali-beta-nginx1,log-ali-prod-nginx1), 根据这些不同的标记准备不同的配置文件,然后根据主机的信息自动判断当前环境,如果是测试环境就自动应用测试环境的配置;如果是预生产环境就自动应用与生产环境的配置;如果是生产环境就自动应用生产环境的配置。

最后一个典型的配置就是关联关系配置,比如A模块要调用B模块的接口,首先就要知道B模块部署在哪些机器上,即对应的ip:port是什么,我们称为endpoint,A模块如果要把B模块的endpoint列表写死在配置文件里,那么B模块要扩缩容就比较麻烦了,需要通知A模块去修改配置并发起A模块的变更,这样关联配置就有点奔溃了...

典型解法有两个,一个是名字服务注册中心,即B模块通过心跳的方式向注册中心汇报自身的endpoint,然后A模块再去注册中心获取B的endpoint列表,如果B模块的某个实例挂了,就不会有心跳了,A模块从注册中心获取到的endpoint列表,就会自动踢掉挂掉的实例。另一个就是加一个转发层,比如Lvs或者Nginx,这样就交给转发层的程序去做判断,做自动剔除和转发请求分配。

WeZan