服务端系统为什么要运维友好?外网事故一直是互联网公司尽量避免的,也是服务端程序员最关心的问题之一。但是,如果我们统计一下各种外网事故的原因,我们会发现一个结论:70%的外网事故是运维领域造成的,只有30%左右的事故是由于运维的BUG造成的。程序本身导致。这里所说的“运维领域原因”包括配置错误、现网运行错误、网络或其他硬件环境变化、硬件故障等。在盛行“运维”与“开发”分离的时代,看似这些都是运维的错,但这个错已经延续了几十年,一直没有本质的进步。说明这一技术问题不能仅仅通过单纯的“区分责任”的管理方式来解决。例如,当您需要重新部署一个包含数百个进程并分为数十种不同类型的服务的系统时,您可能需要小心处理数十个或数百个配置和操作命令。这些配置和命令有大量的相似之处,有些需要仔细辨别异同。此外,这些配置之间还有很多相互关系,您必须非常清楚。另外,这些配置和命令的执行顺序也是有严格要求的。部署这一切就像操作带有数百个按钮的机器面板。最可怕的是,其中任何一个出错,系统都可能立即或在未来不可预知的时间内引发“外网事故”。这导致老板在半夜3:00把你从床上拉到电脑前处理这个烂摊子。当然,我说的这种情况并不是每个项目都会出现,但是我们确实在很多项目中都不同程度地陷入了类似的陷阱。不知道我们有没有被Apache复杂的httpd.conf征服,所以很多程序员都非常喜欢配置文件。“一切都必须是可配置的!”它不仅成为了我们的口号,也成为了无数复杂的配置项和千奇百怪的工具命令。——但这些东西,在实际的商业运作中,其实已经成为了无数的定时炸弹。【程序员爱配置文件】自动化测试现已成为开发的标准流程之一。特别是在敏捷开发方法兴起之后,最重要的实践之一就是自动化测试。我们知道,我们平时用来测试的环境,往往和真实的环境是不一样的。例如,我们在做功能测试时,运行被测程序的内存和硬盘可能比实际运行环境要小很多。如果我们的程序因为这些硬件或者IP地址等其他软件的差异而需要配置才能运行,那么我们的每一个测试都可能需要手动操作,而这样的测试不能说是“自动化的”。再加上人为失误,更容易导致测试结果出现严重错误。除了可能导致运维操作的环境差异之外,测试需要多个环境。比如很多系统有多个分支同时开发,或者有内部功能测试、外部邀请用户测试、公开测试等一个测试环境。假设我们的软件每次部署都需要大量的手动操作,那么在面对多环境和频繁发布新版本进行测试时,部署工作一定是非常繁重的,而这些繁重的工作本来是可以尽可能避免的.【美丽的CI闭环往往毁于复杂的部署过程】快速开发一直是现代软件公司追求的目标。由于需求和市场的不断变化,软件产品和应用系统也被迫每天更新其功能。说到服务端软件,我们在开发过程中往往需要和很多其他程序一起开发调试,最典型的就是和客户端软件进行交互。假设参与项目的每个程序员都集中连接到开发服务器进行调试,那么一定会发生各种相互影响的事情。而且这种跨机开发环境往往只有一些命令行界面,效率不如图形界面的IDE软件。假设我们开发的程序,尤其是服务器端的软件,可以直接在开发的工作机上运行调试,那么除了能够响应速度更快之外,在多个程序同时运行的时候调试也很方便时间,这对于经常因为“联调”而苦恼的工程师来说,是一种非常有效的提高工作效率的措施。但是上面提到的这些措施都需要我们的服务器端程序,可以很方便的部署到各种环境中。反之,有些服务器系统结构复杂,需要启动很多进程,需要配对很多配置文件才能启动。那么大家也就懒得部署多套了,而是挤在一个环境里开发。分布式系统在运维友好性方面的难度,防止了资源泄露。我们知道服务器端程序需要长时间运行,特别害怕资源系列,比如内存泄漏、文件句柄错误、网络连接相关的错误等等。所以很多时候,我们愿意在服务器一启动就“占用”或者“分配”所有需要的资源,然后不管后续请求进来多少,做什么,都不需要“分配”“根本。”,从而杜绝一切“泄露”。但是这种方式也大大增加了程序在运维上的复杂度。首先,我们很难明确硬编码程序运行的硬件资源。相反,我们设计诸如配置文件和命令行参数之类的东西来根据运行时环境确定可能使用的硬件资源。比如我们会在配置文件中设计一个“networkprotocolbuffersize”配置项,根据服务器的内存大小进行配置。然而,程序中的函数可能非常复杂。如果要将内存、文件等所有资源都配置为配置项,则配置文件必须同样复杂。如果我们期望运维人员理解这些配置文件,开发人员自己运维更好,因为开发人员有时并没有想清楚这些资源的合理分配——原因是他们太依赖了很多关于这种“预申请”的方法,而我习惯于把这些棘手的问题推迟到后面去解决。防止资源漏洞固然是一个重要的问题,但是简单地将资源申请变成配置文件也会带来另一场灾难。特别可怕的是,这种配置文件灾难在多进程协同系统中会维持几何倍数增长。这种运维复杂度在一个系统刚上线的时候似乎还可以接受,但是随着系统逐渐变大变复杂,运维工作的难度就像温水煮青蛙一样。完全失控了。有些系统已经运行了3-5年,后来发展到没有人可以从头部署一个新环境的地步。【打个成语】快速诊断故障。今天的商业应用系统往往不是一个非常简单的功能体,而是包含大量相关或不相关的功能。我们最害怕的问题是,这些常用的函数,在同时处理上千个网络请求时,如果某个部分函数代码出现BUG,导致整个系统不可用。所以我们往往更愿意建立某种隔离系统,比如在不同的进程中运行不同功能的代码。这样,利用操作系统的工具,就可以很快找到那些有问题的代码。但是如果真的要将一个系统的多个功能分离到不同的进程中运行,首先会遇到的就是进程间通信的问题。这个问题是现代分布式系统的核心问题之一,无数的开源软件项目都在试图解决这个问题。但是不管是使用开源软件还是自己写代码来解决,这样都会增加系统的进程数。尤其是我们喜欢按功能来划分代码和进程,这意味着在运维一个系统的时候,我们需要面对大量“不同类型”的进程。而且我们把功能划分得越细,流程的种类就越多,需要运维的流程也就越复杂。在管理这些进程时,除了上面提到的一些性能参数需要配置外,还有海量的进程间关系需要配置。而这些进程间的关系会随着业务的变化而变化,这对于那些没有具体接触开发需求的运维人员来说简直就是噩梦。可能有些程序员开始在通信公司工作,所以他们非常习惯按流程划分功能,按通信层级组织系统,但是随着业务系统越来越复杂,这种工作习惯带来了很多麻烦——可能是需要每周向系统添加新的进程,或者调整某些进程的通信关系。不同的行业需要不同的技术方案,这是理性工程师的想法。【小猫喜欢把地面画成监狱,我们的代码也喜欢把进程边界画成监狱】负载均衡。现代服务器端系统基本上是分布式系统。也就是说,多个服务器、多个进程组合??起来提供服务的系统。为了使该系统稳定工作,最常见的措施是防止过载。为了防止多个进程出现一定的过载,需要进行负载均衡。为了防止同类的所有进程过载,需要过载保护。分布式系统中最常见的配置任务之一是配置每种类型的进程启动的数量以及每个进程的过载保护阈值。但是,在一个有上千个进程、上百台服务器的系统中,准确填写这些配置其实是非常困难的。特别是,这些服务器的性能并不像提供商声称的那样一致。如果需要在集群中增加一些服务器,或者修改(搬迁)一些服务器上的服务,那就更危险了,因为稍有不慎就可能导致原来工作的系统出现故障。但是,就像业务需求在不断变化一样,运维环境也在不断变化。比如搬迁IDC,就是最常见的“折??腾”。我们可以写很多运维管理工具来尝试将这些工作“自动化”,但是业务需求在不停地“折腾”,而在一些“开发运维分离”的团队中,开发人员并不是很关心开发运维工具,因为被市场和业务人员逼着加班加点,只想着功能尽快上线。由于负载均衡的需要,大量服务端软件的工作量和我内部的工作量都与集群中庞大的服务器数量有关,所以是运维难度和难度的最直接体现服务器端系统的开发。如何开发一个运维友好的服务器端系统为了让服务器端系统运行良好,我们显然应该采取一些开发措施,而不是简单地依赖所谓的“运维”甚至不太可靠“管理”方法,以减少错误和故障。第一个可以借鉴的思路是“构建具有性能灵活性的系统”。所以,性能弹性,最简单的说就是我们的服务器进程不需要复杂的配置文件,不需要运维操作,就可以运行在各种性能环境中。除了自检测机器IP地址、内存大小等最简单的自配置功能外,更重要的是我们对资源管理思想的提升。由于一个系统要处理的问题可能很复杂,因此需要使用的资源也会很复杂。比如我们需要用内存来缓冲被没收的网络包,还需要用内存来存储用户会话数据等,如果我们只是提出并配置这么一块内存,那么各种内存的配置就会很多能力。但是,我们可以通过建立业务抽象来简化这个资源模型。例如,对于一个在线交互系统,我们可以将资源管理的单位定义为“会话”——每个会话代表一个“并发”服务,我们可以设计每个会话使用多少资源,然后我们注意管理防止资源泄漏的“会话”总数。当然,这种“会话”在不同的业务系统中可能有不同的概念和作用。幸运的是,我们还可以利用面向对象的思想,将这些会话及其相关数据用类和对象进行封装。这样,我们在规划性能的时候,就不用在程序里翻找用到“资源”的机器配置了,只要抓到一个关键变量就可以了。更重要的是,我们可以对“session”等关键指标采用“池化”的管理策略,将对此类对象的使用变成需要“申请/返回”的机制,从而放弃“分配”的做法一开始的大量资源是根据实际需要分配资源。由于“池”的限制,当资源达到上限时,拒绝进一步的服务请求,在防止资源漏洞的同时解决了一些过载问题。保护的问题。而且,在某些环境下,我们还可以让这个“资源池”更加智能化和弹性化。例如,当请求压力接近上限阈值时,我们可以做一些扩容或告警工作,而不是简单的拒绝服务。或者我们可以定期查询已经“申请”的资源的处理情况,如果发现资源占用时间过长,我们可以清理这些服务请求,这样就有一定的灵活性在自我恢复服务中。如果建立了“资源弹性”的系统能力,这样的进程可以用最少的配置进行自我管理和运行。从根本上降低了运维工作的复杂度,也降低了环境变化对系统的影响。同时,抽象好的功能代码对于代码的维护和开发也是非常有利的,可以说是一举多得。【子弹封装火药、弹头、底火,告别马桶式发射】第二种思路是“在功能容器下运行”。在某个项目实践中,我看到了某个系统,它的每一个流程都包含了整个系统的所有功能代码。通过启动时的命令行参数,可以指定这个进程需要提供哪些功能。就运维的便利性而言,这个系统远比需要配置和部署各种功能发布包的系统简单。而且该服务器系统还可以以单进程全功能的形式进行开发和自动化测试,在开发效率上具有明显的优势。在JSP/Servlet技术的使用中,我们往往会部署不同的WebApp运行在不同的Servlet容器中(如Tomcat/Resin等),而没有完全配置各种Servlet容器。现在还有一些系统,主要的业务功能是用python/JS/Lua这样的脚本语言编写的。系统中的流程部署,只要脚本容器(引擎)完成,基本上就是复制脚本文件。.在容器技术的支持下,除了简化部署工作之外,我们还可以获得一些“热更新”的好处。对于基于硬件和流量的运维工作,运维人员可以集中精力管理“容器”。例如,GoogleAppEngine是一个高度自动化的WebApp容器。用户甚至根本不需要安装和部署任何软件。他们可以直接上传一个PHP脚本或者Servlet类文件来开始提供服务。运行在容器下的服务器系统也可以使用容器规定的一些通信规范来做一些自动化运维,比如自动扩容、缩容、容灾——容器可以自我发现集群的运行状态,并添加新的运行资源,去除故障(如访问超时)的运行资源。这也是所谓的SOA概念的最常见实现。从另一个角度来说,如果我们有容器的支持,我们在配置server进程的时候就可以简化整个集群中各种关系的配置,因为只要告诉容器如何加入一个目标集群,其他的事情就可以了完毕。允许容器与其他集群成员协商配置。容器除了提出统一的功能代码开发环境约束外,还规范了运维工作。这对于需要经常改变服务内容,不断改变运行环境的项目来说是非常有价值的。在WEB开发领域,容器的概念已经深入人心,因此这类系统得到了广泛的应用,运维工作也可以专业顺利的进行。然而在网络游戏等没有“行业标准”的领域,函数式容器的概念仍然没有被很多人接受。很多人还在质疑为什么要给自己戴上这个“枷锁”,殊不知自由总是在束缚下行走。【进程需要容器,函数也需要容器。有容器总比没有容器好!】最后说一下各种运维工具,不管是Chef还是各种非通用的运维部署系统。以系统提供的能力,很难统一管理所有的系统。而如果我们在开发的时候充分考虑到系统的运维需求,那么我们可能只实现一些简单的约束,就可以大大提高运维工作。我想这就是所谓的DevOps流行起来的原因。
