内容简介:演讲所对应的业务背景:最近十年,互联网的发展速度越来越快,线上承载的数据流量呈几何级数的增长。
讲师简介
乔斌
阿里巴巴技术专家,百年技术讲师, 应用运维平台新一代架构的主要设计者。十年以上海内外工作经历,曾为多家财富500强企业提供服务。
同时他还是一位社区活跃者,曾在Global Scrum Gathering,Regional Scrum Gathering,Agile Tour上发表主题演讲。
演讲所对应的业务背景:
最近十年,互联网的发展速度越来越快,线上承载的数据流量呈几何级数的增长。
这种增长直接刺激了基础设施的迭代优化,由于供应链持续紧张,新老架构不得不同时对外提供计算能力,由于不同业务方对运维有着不同诉求,这导致操作组合越来越多。
比如:做应用迁移,既可以切流-搬迁-引流,也可以扩容-缩容,也可以直接做Serverless。条条大道通罗马,没有哪条一定对,一定错。
问题在于,在某种情况下,可能适合操作路径A,而在另一种情况下,适合路径B。主观判断的不确定性随之放大,这给运维平台的稳定性保障带来了极大的挑战,同时也让我们认识到,面向过程与操作的的运维模式将难以为续。
面对这样的处境,业界巨头其实早已开始了这方面的探索。典型系统有微软的AutoPilot、Google的brog(K8S),阿里的Apsara Infrastructure。这些系统的背后,都具有一个共同的基本原理 - 面向终态。
终态描述了系统的最终形态,用户只需要描述自己想要什么,而不必再心惊胆战的规划执行路径,之后系统会根据线上实时状况以及知识库来动态调整,使命必达。
本文将从较抽象的层面介绍了系统建设中的一些关键点。文中的某些观点,完全基于其大规模运维场景的前提假设,如果换成中小规模的运维平台,则不一定适用。
1. 业务背景
将运维的原子操作串接成一个可以解决问题的工作流是上一代系统的主要设计思路。这种设计方法最核心的部分在于他的工作流引擎,它帮我们用较低的成本把运维操作串接起来,在很短的时间内,大幅降低了线上操作失误的可能性,实现了其阶段性的价值。
基于工作流引擎的运维系统工作简单,性价比高,但其在运行稳定性上是有缺陷的。这与运维系统的特点息息相关。
就单机系统而言,其运行环境是极其稳定的。命令行输入hello world,输出终端就会显示hello world。要么成功,要么失败。运行一万次,其系统表现也一模一样。而运维系统的运行环境却有着很大区别,它是一个典型的分布式系统。
一个复杂的运维系统,由主控端、操作者、受控对象这几部分组成,各个对象皆可能部署在数个集群。 在运行过程中,主控者与受控对象皆有可能受到网络、服务器等线上环境波动的干扰,也就导致了线上结果的不确定性。 比如:某监控项设置的是99%的agent能在40秒内正常应答,则认为该region正常。但,某刻因为网络延迟的原因,在40秒的时候,只有92%的agent应答,但其实这些agent全部是正常的,将在下一秒返回结果。这就是环境对结果影响的一个例子。
基于线上环境不稳定的事实,大家开始从各方面着手,寻找应对不确定性的方法。最近两年讨论比较多的是基于恢复的模型设计。
在开始解释这种模型之前,有必要介绍两个关键概念MTBF与MTTR。
MTBF,平均故障间隔时间(系统正常运行时间)。我们平常做的code review,故障复盘后的优化措施。本质上都是为了提高系统正常运行的时间。
MTTR,平均故障恢复时间(系统从故障中恢复的耗时)。我们平常做的各种回滚,都是为了更快从故障中恢复。
故障造成的损失与其持续的时间是息息相关的。一分钟的故障,造成的损失尚在可控范围之内,但一天的故障,可能对企业造成毁灭性的打击。所以,基于恢复的模型是在努力减少故障造成的破坏面。
在实际的操作过程中,基于恢复的模型有一个非常致命的弱点,那就是其恢复的成功率与线上环境的复杂程度成反比。比如:由 Docker 与 MySql 实例组成的环境,如果数据库被删除且之前有预案,那么很快就能恢复业务。但,如果你的数据库本身是由十几个团队开发出来的云产品呢?那么恢复的复杂程度就要高很多。也许数据库实例与数据很快就能恢复,但其所对应的加密通信网络能很快恢复吗?其他组件呢?
基于恢复表现与复杂度的反比关系,在构建基于恢复体系的同时,也得想方设法维持运行的时间。于是,我们想到了基于终态的系统架构。
就提升MTTR而言,虽然,从设计上基础设施描述与应用描述解耦,交叉变更,但系统提供了类似time machine的功能,它会记录上一次正确运行时刻的基础设施与应用配置基线,以便恢复时能快速get到正确的恢复点。
在发生故障后,系统会推送上一个正常状态的终态描述文件,该文件既包括了基础设施描述,也包括了应用信息描述。文件推送后,期望状态已知,监控系统实时反馈当前状态,系统不断执行终态逼近,直到期望状态与当前状态一致为止。
就维持MTBF而言,监控系统会不断依据终态描述文件描述的期望状态来巡检线上系统。发现不一致的地方,则主动补偿维持,防止风险聚集,延长系统正确运行时间。
从业务视角来看,不同的子公司会有不同的业务,不同的基础设施。从运维实例视角来看,又有应用与中间件,有些有状态,有些无状态。这些都需要在一套框架中得以满足。
讲到这里,我们可以做一个描述性的目标小结了。我期望建设一个面向终态的系统,它可以支持公司的多种业务,跨不同的IaaS,支持有状态与无状态运维,最终缩减故障恢复时间,提升系统运行时间。
2. 架构详解
说的很玄乎,但如果将终态架构用白话描述,其实很简单,过程非常类似大家的绩效达成过程。年初时,为团队定KPI,接下来团队成员各显神通,向着目标前进。过程中,定期向你汇报结果,作出动态调整,最终达成KPI。该过程如果以架构式的模式来描述, 会包括四部分:
-
控制器:负责制订KPI目标,将分解后的目标写在纸上
-
执行器:各个执行器负责执行分解后的目标
-
反馈器:定期上报分解目标的执行状态
-
如果发现某目标的期望状态与反馈器的实际状态不符,控制器会决定是否新建一个目标来进行补偿修复,循环往复,使命必达
有没有发现这个过程非常类似管理上的PDCA环?
回到系统中来,让我们通过部署升级的例子来熟悉一下其运作过程:
-
Step 1 :用户输入终态文件,描述哪几个集群的镜像,期望成为v2这个版本
-
Step 2: 终态配置文件被输入到变更队列,等待执行
-
Step 3 :该终态文件被从队列中读取,解析成为实例级别的变更列表。比如:集群1、2要达成v2版本,解析出来就是容器1、容器2….. 容器x需要达到v2版本
-
Step 4 :DeploymentController轮询运维实例表,发现”容器1-容器x”待升级
-
Step 5 :DeploymentController动用一切可能手段,将每个容器升级为v2版本
-
Step 6 :监控系统上报每个容器的当前状态
-
如果上报的某容器当前状态是v2,则完成目标,如果不是,比如上报状态为error。然后AutoHealController会自动轮询,并执行修复。
接下来,让我们简单了解一下系统的抽象架构以及各组件的职责:
-
Portal: 面向业务视角的表单,用户借此提交变更请求
-
API Server: 后台与Portal以及OpenAPI的交互层。用户输入完成权限验证、静态安全检查、限流、审批操作后,将合法用户需求转换成终态SPEC输出到Brain
-
Brain: 将用户输入的终态指令根据运维安全规则,拆分成1个或多个顺序执行的SubSPEC。 SubSPEC在执行时,会被翻译成每个实例需要达成的状态,并写入Master。
-
Master: 存储了运维实例所需要达成的期望状态,以及当前状态等信息。各组件通过Master来进行交互
-
Controller:读取Master所描述的各实例的终态信息,并逼近终态
-
Decider:负责在逼近终态的过程中,根据线上状态决定是否批准一个运维Action。
-
Monitor: 巡检各个实例,正确上报各个实例的当前状态
在执行状态变更的过程中,如何确保运维安全呢?这就依赖于安全模型,该安全模型由Brain中的Decider组件、Master、Controller、Monitor这几部分协作完成。
-
Step 1 : Controller如果发现有3个容器需要升级成为V2版本;
-
Step 2 : Controller并不会直接开始更新。这样太鲁莽了。它会先提出更新请求。将请求写入Master;
-
Step 3 :Decider轮询Master,发现有人提出了Deployment的请求,然后它开始观察该应用的全局情况。
比如:该应用本身有10个容器,并且10个容器都处于正常运转状态,流量与延时等各方面指标皆正常。所以,它批准了请求。反之,如果该应用有10个容器,但5个已经处于瘫痪状态,再更新3个会有极大容量风险,那么Decider会驳回请求;
-
Step 4 :Controller轮询Master,发现有请求被批准。于是开始执行更新操作,执行完毕后,通知Master,任务完成;
-
Step 5 :Monitor不断监控每个实例,上报每个实例的版本。比如:刚刚的3个容器就会被Monitor扫描到,并上报其当前实际状态;
-
Step 6 :如果Monitor巡检过程中,发现刚刚的三个容器,全部没有响应,则会上报真实状态Error状态到Master;
-
Step 7 :AutoHealController发现3个容器状态为Error。则会请求执行自愈操作。等待Decider批准后,开始执行自愈。如果自愈成功,则再次执行部署或者其他动作,直到达成V2为止。
在上面的安全模型中,运维安全性的高低,由如下两个非常重要的因素来决定:
-
因素一:运维实例的拓扑结构。变更本身是一个牵一发而动全身的事情,系统需要从一个变更上,推导出多少实例被影响。比如:升级一个容器,那么被影响面很小。但升级一个DNS,那么影响的就不仅仅是一个DNS,很多个容器,VIP有可能都会包括在被影响拓扑中。你需要识别出这种关系,那么Decider才能作出正确的判断,它全盘观察运维风险,判断哪些操作可以被放行
-
因素二:应用画像。应用画像是一个描述应用健康度的指标,监控系统根据这个画像来综合判断该应用是否正常。画像可能包括版本、基线、网络延时、有无业务流量进入等等等等。不能单一根据镜像版本等价与否来判断是否成功。比如:虽然版本已经成功升级,但是无任何业务流量进入,或者延时非常高,则有可能判定为升级失败,因为它不满足业务正常运行的描述
这两个因素,通常都是在落地过程中,比较难推行,但又非常有价值的事情。
前面说了这么多,需要做个总结。这里构建的是一个系统,它能输入终态目标,系统会想办法达成终态目标。它的优势在于,它是一个协议框架。也就意味着可以广泛和其他团队合作,如果有精力,就尽可能多的实现终态逼近的Controller。 如果没有,发消息,让其他团队实现也行。
只要最终反馈器能检查其执行状态,那么就谁做就不再是关键路径。并且整合过程中很大的好处在于,甚至不需要改变DNS原系统的实现逻辑。抓到老鼠(Monitor检查后状态为true)才是判断系统好坏的标准。
我一直认为好的模型,它一定是逻辑非常简单,但实现可以是复杂的;而不是逻辑就非常复杂。
终态系统就是这样,它的逻辑非常简单,但落地起来,却较为复杂。它的复杂来源于其现实中的各种情况。比如:
-
企业的难点在于技术多样性。比如:企业里面除了Java,还有C++、 GO 等等
-
企业的难点在于历史包袱。比如:有部分搜索应用,可能在相当长的一段时间内,比较适合物理机
-
企业的难点在于业务优先。比如:不能完全当成无状态来处理,升级过程中,一定要按照特定的分组顺序来升级,减少切换流量带来的线上风险
-
企业的难点在于需要一揽子解决方案。比如:能不能把公有云、专有云、输出等场景做成一套系统,不要换个场景,就要额外做很多适配
-
企业的难点在于现实世界有着诸多限制。比如:只能腾出五个人来建设这套系统。这就是限制。
系统建设的复杂性,不体现在其架构,体现在你真正去实现,去面对它的时候。
3. 现实问题
开始设计时,我们首先考虑的是系统的复杂度分配。这么大的一个系统,其客观复杂度是存在的,不在这就在那。所以,需要根据自己的人力结构,来分配系统的复杂度。
曾经我们考虑过做成最简单的Master-Client类似的结构。这样的好处在于,它的逻辑结构非常简单。实例期望状态写入Master,宿主机上的Client直接轮询Master,在宿主机上下载指定镜像即可。
如果采用这种结构,复杂度的分配就在Master上。参考兄弟团队建立天基平台的经验。要保证Master能承载百万级的并发,并且在分布式的情况下,保持数据一致性。要投入很大的人力。同时,Client要兼容不同的操作系统,不同的版本、不同的 python 版本,也需要大量的人力。所以,在盘点完人力后,我们没有采用这种方式。
取而代之,我们采用了另外一种方式Master-Controller-Agent的模式。让Controller去轮询实例,确定要做的任务,然后通过Agent通道发送命令来执行。这样的好处在于,由于Controller轮询可以控制批量以及轮询时间节奏,可以有效分散Master的压力,同时Agent在集团已经有相当成熟的组件,可以为建设过程节省大量的人力。
同样的原理,也适宜于Controller的封装。理论上来说,白名单控制,CMDB修改状态,这些细小的操作,都会影响运维的行为,都可以潜在成为一个运维Controller,但在具体落地时需要权衡。因为,Controller的粒度越小,虽然控制粒度越精细,但主控端就会越复杂。执行器的精细度与主控逻辑的复杂度存在很明显的反比关系。
越精细,就越优美,但是越容易犯错,所以权衡后,我们把一组能解决运维问题的action放在了一起,封装成为了一个Controller。基于这个粒度,主控与被控达到了很好的平衡。
之后在用它解决具体问题时,很快发现理想与现实的差距。从K8S、AutoPilot等系统的设计上,你能发现它们整个设计都是围绕无状态设计来运作的。这样能有效降低系统和后续运维的复杂度。但现实世界本不是如此。比如如下业务问题,就是很好的例子:
-
对于同样一个Pouch分组,我要扩容5台时,并不关心机器。但在缩容时,要指定机器怎么办?
-
日常环境蓝绿时,我只选择蓝绿部署方式就可以了。但线上部署时,我一定要先部署CENTER.unsh分组,然后再部署CENTER.unshyun分组。怎么办?
-
机器自愈、机器置换时,我一定要用某台机器来做置换,怎么办?
上面的例子,就是现实。对于有状态的场景,处理起来比较简单。当用户想缩容时,只需要简单把数字减1,比如:想把容器从3347变成3346,只需要简单定义分组容器数字为3346即可。少于3346,系统会自动执行扩容,多余3346,系统会执行缩容。
如果用户需要在缩容的过程中,指定缩容的机器呢?比如:用户想执行缩容,同时集群中,某组容器所在的宿主机属于非标规格,想优先缩减该宿主机上的容器。
这样一个无状态的场景就变成了一个有状态的场景。为了应对这种情况,我们引入了运维参数的概念。让用户可以在达成最终状态的过程中,设置运维运行时的行为。这就很好解决了有状态的问题。
由于有状态与无状态是一个大家非常感兴趣的概念,所以我们可以适度展开讨论。
首先要了解,运行时与编译时是两种完全不同的状态。以K8S为例,比如:你要将集群达成v2版本。这时需要定义K8S Yaml配置文件,推送到系统执行。从文件描述来看,肯定是无状态的。K8S正是基于此来设计系统。但,在运行过程中,如果线上有问题,那么管理员首先考虑的是如何快速解决问题。圈出一批机器先重启一下,这是非常常见的操作。这时,一定会涉及到对运行的指定实例的操作,这是个有状态的行为。
他们的关系非常像程序中类与实例的关系。从编程的角度来看,类是一种描述,是无状态的;实例是类的特例,是有状态的。
总结起来,我们可以看到,应用描述是无状态的,但运维是有状态的。无状态是一个结果,但有状态是过程。就一个终态运维框架而言,既要支持无状态的描述来描述结果,同时又要对运行的过程进行有状态的定义,控制运行过程中的行为。支持这两个方面,才能构建出一个适应性较强的系统。
应用运维平台是一个全局性的系统,它处于业务层,需要整合运维能力,最终为业务服务。为了服务好业务,就需要具备全局视角,前面讲的Decider组件,在决定是否批准某运维操作时,就基于对全局的判断。这就需要掌握全局数据,且理想情况下这些数据都存在共同的地方,在运维领域,它叫CMDB。
CMDB应该是公司最权威的运维数据来源地。各个系统都往那里存数据,也从那里取数据,共同保证数据的干净与整洁。CMDB要能成为这个权威中心,就需要CMDB团队具备快速支持业务的能力。否则,网络团队要某种维度的查询,但如果CMDB不能快速响应,或者网络数据的保障级别是5个9,但CMDB的保障级别是4个9。在类似情况下,CMDB的权威性就会受到挑战,最终助推各个运维系统做自己的保障,进而产生了多个CMDB中心。这就会最终导致全局规划时需要触达多个数据源,处理多个数据源中的数据一致性问题,导致复杂性成倍提高,最终影响落地。
所以,CMDB如果不能得到高保障,就很容易成为全局规划的阻碍。
在上面问题都解决之后,就可以考虑系统级别的容灾问题了。在容灾上,应用的多region部署,相对比较简单,这是由应用的无状态所决定的。最难的部分在于解决数据库单点的问题。对此,至少有两种典型方案备选:
方案一 :单元化部署。在每个region构建完全对等的部署体系。当用户访问发生时,将访问路由到各个region,每个region只处理一部分数据,并最终保持数据一致。
它的好处在于,数据库不会成为瓶颈,可以水平扩展。但它的缺点在于,复杂度高,一旦发生灾难,必须重新修改路由规则,让剩余region均分灾难region的流量。
方案二 :数据库主备互切。应用部署在多个region,但数据库依然以中心化的模式运作,其备库有数个,部署在不同区域。当灾难发生时,如果备库失联,则无影响。但如果主库失联,则切换到备库,继续运作。
它的好处在于,技术实现上非常简单,不会增加太多开发上的工作量。但缺点也很明显,如果是因为数据访问本身导致的故障,切流后,依然会把备库打爆。
最终,出于简便性的考虑。依然选择了方案二,然后从应用层解决限流的问题。
听起来,永不停歇的服务是一个很高大上的能力,它减轻了灾难对业务连续性的影响,这也成为了很多公司宣传上的点。灾难确实很可怕,系统瘫痪也很可怕,但对于运维系统而言,瘫痪却还不是最坏的结果。
最怕的是,别人都瘫痪了,你没瘫痪,但是因为主备库中间有100ms的延时,缺少了一丁点数据,导致你做错了,这就有可能造成比瘫痪更恶劣的结果。
也许第一直觉你会想到,这个很好解决,做幂等可重入就行了。如果之前没做过,就再做一次,做过了,就跳过。但在现实世界中,对大型平台,幂等性几乎无法落地。幂等性的原理决定了双方必须持有某种token,根据业务token来判断执行状态。这就需要各个系统都在开发过程中,严格考虑这个点。当系统的数量以百计,并属于多个团队时,几乎就成了一个无法完成的任务。
最终,摆在我们面前的选择,只剩两种,选择强制一致还是最终一致性并做幂等。
他们各有优缺点:
强制一致性,需要多写数据库,会造成一定的性能损失,影响用户在使用系统时的体验。但它的优点是,数据一致性非常有保证。再也不用担心灾难时丢数据的问题,并且也不用开发。
最终一致性。用户不需要承担性能损失,但有可能在灾难时,丢掉小部分数据。如果保证数据不丢,则得做幂等性开发,大幅提升落地工作量。
最终权衡后,我们选择了强制一致性与最终一致性的混合方案。在对数据进行分类后,我们发现,80%以上的数据,其实丢掉也没关系。比如:实例状态数据,就可以在灾难后让Monitor进行一轮实时上报即可。少部分工作流数据,做强一致性。由于工作流是一个低频业务,接受一定延迟也可以接受。
另外,选择对部分数据做强制一致性的原因,是因为运维系统本身对稳定性的严肃态度,安全才能运维。
在设计过程中,我被反复问到为什么不用K8S的问题。这强迫我反复思考这些问题。
其实,K8S框架中提供的K8S + CRD + Operator的方式,确实可以解决一部分将有状态场景无状态化的问题。但最终我们只选择了兼容,而没有再其上进行构建的原因,还是基于当前业务现状的考虑。
假设采用了上面的方案,可以很好解决Deployment问题域下的问题。将中间件、应用以一种模式来进行部署。
但对于运行时问题,K8S支持的不是很好。现实业务中,存在很多按指定分组发布、采用特定端口、设置指定机器等一系列情况。如果采用K8S,要做大量的定制化开发,很容易成为一个K8S’,它看起来会非常非常像K8S。但其实不是,K8S’已经是一个完全不同的物种了。
这让我开始深入思考“标准”这两个字的深刻含义。
什么叫标准?
车同轨、书同文是一种标准。焚书坑儒,也是为了达到标准。
车轨同宽,货物就可以很迅速的到达七国,而不用每到一个国家就换一辆车来适配。
对部署而言也是一样,如果下面的基础描述都一样,那么部署很容易就统一全集团。
但问题是,车要同轨,各个国家的路,由谁来修呢?什么时候修呢?要各个业务方来改造业务吗?现在就停下部分业务来做运维改造?
所以,你会看到运维人员对K8S的热情很高,开发人员就少了一点。说到底,还是由谁来做脏活累活的问题。标准本质上做的是复杂度重分配的事情。从选择上,基于我们对业务重要性的理解,我们选择了自己做Adaptor适配,而不是要业务使用强制标准。而既然要做太多自定义,也就难以享受K8S标准所带来的好处了。
但我们希望,在集团上云后,基础设施层会越来越趋于收敛,那时,我们会再度考虑标准化的事情,到那时,也许会是一个合适的时机。
4. 眺望未来
关于未来,我们也想过AIOps的事情。运维是存在某些基本安全原则的,比如:部署时不能重启,但部署时,可以进行扩容。这些规则存在某种互斥或者相容的关系。如果让Brain对这些规则进行定义,是不是非常类似典型的狼草羊人过河的问题。算出它的最优解,就是一个最佳执行路径。
但在实际过程中,甚至最近几年我对AIOps案例的观察。在运维领域大获成功的例子,相当少。为了做到AIOps,你需要做非常多的事情,至少包括:
-
流程线上化,且需要闭环。否则数据极易被污染
-
数据标准化,不能这里用逗号分割,那里用句号分割。这里打在json,那里分割十列。数据不标准会导致做深度学习时要耗费大量的力气去做数据清洗,那简直是个灾难。
-
经验沉淀,寻找最可能的指标集合。以数据中心能耗为例,空调温度、冷凝塔功率、水管流速、水管水量等数百个指标都有可能会影响最终能耗。但计算能力是有限的,很难把全部指标纳入计算。那么需要根据经验沉淀出最有可能的关键指标,然后做深度学习。当预测曲线和真实曲线接近时,再开始做调指标的尝试,然后再投入实战。
这听起来很复杂,而事实上也是如此。深度学习的建设过程会相当漫长,在业务发展如此迅速的当下,可能并不一定适合太多人,太多场景。更多是点上的优化,而不是面上的革命。
相比深度学习的长周期,也许你会发现周边的团队已经有了能快速见效的手段。以前发现自己做了很多troubleshooting,最终很兴奋的定位到最终根因,却发现原来客服小姐姐早就知道类似问题应该找A团队的张三还是B团队的李四了。因为他们已经回答了很多用户疑问,在斗争过程中,早就知道了是谁的锅。
在解决运维问题的过程中,解决路径的长短,是非常重要的一个参考因素。假设需要做一百步,才能解决问题,我认为该方案落地的可能性微乎其微。从这方面来说,身边的兄弟团队的经验和智慧会是比深度学习更重要的力量。
就解决路径而言,我们更需要一种类似知识库与语义分析的框架,类似终态的框架,它让我们可以把能力聚集起来,发挥极大的能量。在这点上,我爱聚沙成塔的框架胜过人工智能。
说明 :以上为阿里乔斌老师在 GOPS 2019 · 深圳站的分享。
乔斌老师的分享还没看够?互联网金融场景下的运维如何做?
7月5日~6日,2019 年 DevOps 国际峰会 · 北京站,看蚂蚁金服技术专家肖袆、国泰产险专家张君为您共同分享~
精彩视频详细介绍 ▼
点击 阅读原文 ,立即订票
以上所述就是小编给大家介绍的《阿里老专家:超大型运维平台的面向终态设计》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:- 快手超大规模集群调度优化实践
- MySQL自增id超大问题查询
- 超大屏数据可视化总结-智慧城市
- 超大7k高清显示器显示网页解决方案
- 超大文件上传之计算文件MD5值
- PHP超低内存遍历目录文件和读取超大文件
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
Letting Go of the Words
Janice (Ginny) Redish / Morgan Kaufmann / 2007-06-11 / USD 49.95
"Redish has done her homework and created a thorough overview of the issues in writing for the Web. Ironically, I must recommend that you read her every word so that you can find out why your customer......一起来看看 《Letting Go of the Words》 这本书的介绍吧!