从运维视角来看何为好的软件架构
这是在滴滴内部混沌说meetup上分享的内容《DEV和SRE的换位思考》的上篇,欢迎交流探讨
转载自我自己的公众号:https://mp.weixin.qq.com/s/23TomQBc6VkpnRDxjHFNJA
从软件的整个生命周期来看,写完第一个版本上到线上,只是万里长征走完了第一步,很多研发人员就觉得OK了,大功告成,然后就想着去干个别的新鲜的项目,这个做法显然是不推荐的,如果时间允许应该把这个项目继续优化,从运维角度来提升可运维性,提升稳定性和容灾能力,这可是一个架构师必须掌握的技能,写一些零零散散的业务逻辑真的不如好好提升架构能力。当然,广泛的领域知识也是很重要的。领导对你的诉求肯定是既要又要还要,哈哈阿门。
从运维视角来看何为好的软件架构,一句话概括:就是不需要运维操心的,或者尽量少操心的,总结起来有四个易于:易于部署、易于扩容、易于监控、易于恢复。
易于部署
更少的环境依赖
依赖环境就意味着依赖运维的初始化,依赖别人的前置工作,但是别人是可信赖的么?这是一个典型的风险点。
最典型的减少依赖的方式就是减少系统依赖和库依赖,比如Google的所有C/C++程序都使用静态编译,就是为了减少依赖,减少动态库的版本冲突,对于agent类,这点体会尤为明显,因为agent通常要部署到很多机器上,不同的线上机器环境各异,什么情况都可能出现,少个包,少个命令,太正常不过了
解决环境依赖的典型利器就是Docker Image,容器技术其实出现的很早,为啥到了Docker才火起来呢,就是归功于Image这个机制上来了,没有Image,Build、Ship、Run则无从谈起。除了Docker Image,另一个解决依赖的手段是Buildpack机制,在Heroku和CloudFoundry上可以看到,Buildpack其实就是把用户代码编译之后,和依赖一起打包,比如Java Web程序,Buildpack会先把源码编译打包成War,然后和Tomcat、JDK一起,打成一个包,称为Droplet,生产环境部署的时候就是直接去分发Droplet即可。
自动化的配置
自动化配置,是减少心智负担的典型手段,如果无法做到自动化配置,尽量让所有实例的配置相同,最烦的就是一个机器一个配置...
比如自动探测机器的运行环境,自动配置线程数,根据内存自动设置JVM参数等,都是典型的自动化配置的方式,当然,如果应用运行在容器环境,就要注意了,容器的隔离性比较差,获取到的配置信息实际是所在宿主的配置,如果以此为依据设置一些参数,比如JVM内存相关的参数,很可能会造成OOM
另外就是不同的环境不同的配置问题,如果有中心化配置自然比较好,如果没有,最典型的做法就是把不同的环境配置分别准备一个配置文件,然后根据机器的一些信息自动判断当前的环境,如果当前是在生产环境,就自动应用生产环境的配置文件,如果当前是测试环境,就自动应用测试环境的配置文件。那怎么判断当前所在环境呢?比如我们可以让机器名做的规范一些,生产环境的机器都要在机器名里带有prod字样,而测试环境的机器,都要在机器名里带有test字样,以此,便可非常方便的区分了。
最后一个典型的配置就是关联关系配置,比如a模块要调用b模块的接口,首先就要知道b模块部署在哪些机器上,即对应的ip:port是什么,我们称为endpoint,a模块如果要把b模块的endpoint列表写死在配置文件里,b模块要扩缩容就比较麻烦了,需要通知a模块去修改配置并发起a模块的变更,崩溃...
典型解法有两个,一个是名字服务注册中心,即b模块通过心跳的方式向注册中心汇报自身的endpoint,然后a模块再去注册中心获取b的endpoint列表,如果b的某个实例挂了,就不会心跳了,a模块从注册中心获取到的endpoint列表,就会自动踢掉挂掉的实例。另一个就是加一个转发层,比如lvs或者nginx,这个比较容易理解不再赘述。
易于扩容
尽量做到无状态水平扩展
无状态水平扩展,是最容易运维的服务形态,一般web服务都是这个类型,容量不够机器来凑,无需担心容量上限,因为架构上无状态可以通过加机器提升服务容量,代码性能烂一点问题也不大...
负载均衡接入层在某实例挂掉时会自动摘除异常实例,服务不会受损或受损较轻,如果追求极致,在做服务发布变更的时候,可以先摘掉部分实例的流量,然后去升级,升级完了再加回去,以此避免服务发布时实例重启造成的短暂请求失败。
有状态则自动路由流量
比如不同的实例有不同的状态,有些请求只能交给leader处理,那可以引入选主机制,再slave节点收到请求之后转发给leader节点,或简单的将leader信息返回,让客户端去重新请求leader
本地数据通常是造成有状态的根因,数据如有分片机制,扩容的时候尽量减少数据迁移,比如在中心存放数据key和数据location的对应关系,如果用算法来计算数据key对应的location,比如一致性哈希或者ceph的crush算法,则扩容会导致数据位置发生变更,导致数据迁移,迁移则要能控制迁移速度,避免影响正常请求的I/O,搞过ceph的对这一点应该印象深刻
尽量自动注册进系统
需要避免扩容了某个组件,还要去修改其他组件(接入层除外)的配置方能生效,这个对运维是不小的心智负担,复杂就带来了风险。
易于监控
关键指标透出
每个模块都要透出能够反映自身是否健康的指标数据,比如Google的所有服务模块,都会起一个http server,开一个/varz接口,访问这个接口,就会返回该模块的监控指标数据。当然,这个规范比较强,如无法提供/varz接口的方式,也可以内嵌监控埋点的sdk,或者直接打印日志。
何为关键指标?比如MQ服务的消息堆积量,比如支付模块的微信/支付宝支付成功率,比如对象存储的接口延迟。只有进程是否存活、端口是否在监听这样的监控指标远远不够。
日志规范全面
谈到监控不得不谈日志,最常见的业务监控方式就是日志,所以日志得全面,级别得用对,日志级别定义请参考《日志级别规范》。公司需要制定统一的日志规范,便于统一的自动化工具处理(比如日志收集程序),日志格式要能够轻易用正则表达式抽取指标数据,便于后续监控策略配置。
易于恢复
自动容灾
典型的比如系统自动检测到某个机器挂掉,或者某个盘不可用,或者某个实例异常,自动处理;如果是无状态服务,前端自会有负载均衡器(比如lvs、nginx、haproxy)做这个事情,当然,实例挂掉的时候,client很容易出现请求失败,此时一定要做重试,而server端的接口则一定要做到幂等,负责client也不敢重试啦。
另外就是能够退化处理,比如原本从redis读取数据,速度快,redis挂了可以退化到mysql读取。
能够自保
比如流量过载,能够限流熔断,那要做限流首先得知道模块能抗多大的量,这就要求模块在上线之前有个压测数据,根据压测数据来配置限流阈值,提前压测得知某个模块的流量上限这个要求可以考虑放到运维准入标准里。
能够降级,比如原本网站有100个功能,扛不住的时候可以关掉部分边缘功能,减少系统负载,保证主流程畅通。
外挂自愈
适用于某些故障有固化的处理脚本,并且公司有故障自愈平台,可以用监控告警触发这个脚本。在我司这种外挂自愈的任务每周跑几千个,可以大幅释放运维人力。
举个简单例子,我们可以配置各硬盘报警,使用率80%时触发P3告警,不配置接收人,只配置自愈触发逻辑,让自愈脚本去清理硬盘无用日志,使用率85%时触发P2告警,配置接收人,正常来讲永远不会触发,因为P3的策略会自动化处理,让硬盘使用率降下来,但是如果自愈系统出现问题,就需要这个P2的告警策略来兜底了。