高级计算机编程中的可移植设计是同一软件在不同环境中的可用性。试想你的软件产品需要从Windows移植到Linux,移植到Android,亦或是IOS,从x86移植到PowerPC, ARM,你该怎么做? 可移植性的先决条件是应用逻辑和系统接口之间的泛化抽象。 当为多个计算平台生产具有相同功能的软件时,可移植性是开发成本降低的关键问题。

水平和垂直抽象

泛化抽象的一个核心方法是水平和垂直抽象(拆分),我们回顾一下像 J2EE 和 .NET 这样的技术平台的基本设计原理并与新兴的由各种因特网标准组织定义的 Web 服务体系结构是怎么做的。

J2EE 是一个规范,这个规范定义了使 Java 语言编写的应用程序组件能够被部署并且允许它们利用一些关键服务(如安全性、事务、异步消息传递等等)来运行的基础设施。用他们自己的工具来实现 J2EE 技术的供应商需要为那些服务实现一个共同的核心应用程序编程的接口集。这样在理论上允许遵从 J2EE 规范的组件能够从一个平台实现移植到另一个上。目标是,并且一直是,把业务应用程序从各个操作系统和硬件技术中分离出来。

图(一)

你可以从上图(一)中看到所有可移植组件开发环境都遵循的基本模型:一个应用程序组件通过一个由供应商的特定实现支持的公共编程接口集来与系统服务进行交互。为了集成安全性、事务、数据库调用或者其他必需的工作而对这些公共编程接口进行编程,使得当底层的操作系统或者硬件更换时,业务组件仍然可以正常工作。归根到底,可移植性就是把业务应用程序使用的接口从它所运行的底层系统中抽象出来。然而,可移植性不会把应用程序与其他应用程序通信使用的接口抽象出来。图(二)

上图(二)展示了 Web 服务技术在抽象业务应用程序的通信接口中所扮演的角色。 在这里你可以看到应用程序在业务应用程序间水平关系上的设计理念(即,通过使用公共的其他应用程序的抽象接口,一个应用程序可以具有互操作性),它与在垂直可移植组件模型中看到的设计理念(即,通过使用公共的系统服务抽象接口,一个应用程序可以变得可移植)是一样的。如果没有这些水平抽象,运行在多个平台上的应用程序就不能与另一个应用程序通信,这是因为这些平台被组合在一起的方式有很多不同。就像 Java 和 J2EE 技术允许把一个业务应用程序从一个技术平台移植到另一个上一样,Web 服务允许将运行在不同平台上的业务应用程序集成在一起。

容器化技术

除非完全不了解云技术和云本机应用程序开发,否则你或许听说过 Linux 容器和在过去两年来迅猛发展的 Linux 容器和基于容器的项目。如果没有听说过它们,那么你可以将 Linux 容器视为轻量型的虚拟机,从而可以更灵活地使用、更快速地继承和更容易地移植它们到不同的系统平台,甚至云平台。Docker 是在这方面走在前沿的项目之一。自 2012 年启动以来,Docker 团队(现在已是公司)提供了一种通过 Linux 容器构建、打包和分发云本机应用程序的非常简单的方法,Docker可以部署在Linux、Windows 和 Mac系统上。

容器与虚拟机有何不同?每个虚拟机(如下图中的左侧所示)运行自己的操作系统实例,并提供它自己的库和二进制文件。容器(如右侧所示)是隔离的,它们共享底层的主机 OS 和库,只打包必要的应用程序二进制文件。Docker作为轻量级的基于容器的解决方案,它对系统侵入性低,容易移植,天生就适合做自动化部署,这些特性非常有助于降低构建持续交付流程的复杂度。缺乏良好设计的镜像会给日后的维护带来较大的问题,我们需要注意它的设计点。

Docker镜像分层设计

为了重用 镜像(Image),加快运行速度,减少内存和磁盘的占用,Docker container 运行时所构造的运行环境,实际上是由具有依赖关系的多个 Layer 组成的。如图 1 所示,每一串数字 ID 就代表了一个 Docker Image Layer。当我们在拉取一个 Docker Image 的时候我们会发现所有依赖的 Layer 文件将会被下载。

例如一个镜像的运行环境是在基础的 Docker Base Image 的基础上,叠加了包含例如 JDK 等各种工具的镜像,再叠加包含第三方库及其相关依赖库的镜像,以及包含了 J2EE 应用的 EAR 包的层。因此,当每次由一个基础 镜像创建后,新镜像就自动增加了一层。

企业内部都存在一套标准化的规范,在这套规范中定义了开发中所使用的语言、工具的版本信息等等。我们可以依靠这份标准化规范,创建一系列容器并把它们按照不同的职能进行分组。

Docker注意事项

  1. Dockerfile开头的 From 和 MAINTAINER 其实都是一层镜像
  2. 如果 From 和 MAINTAINER 不同,就算是后面的命令语句相同也不会是相同的镜像。如都是执行RUN echo "hello world" >> test.txt,如果 MAINTAINER 不同,则生成的这个语句的镜像层将是不同的。
  3. 原理上如果每一层对应的父层不同,那怕执行的命令相同,Docker也会生成一层新的镜像,如下面两个Dockerfile文件

Dockerfile 1:

FROM centos:latest
MAINTAINER [email protected]

RUN echo "test" >> hello.txt
RUN echo "hello" > test.txt

Dockerfile 2:

FROM centos:latest
MAINTAINER [email protected]

RUN echo "hello" > test.txt
RUN echo "test" >> hello.txt

这两个文件的内容只是两个 RUN 语句顺序不一样,但是最后它们生成的image层是不一样的,可以通过docker history <image name>来对比。从这里面也看到一个问题,From最好不要用lastest标签,避免不同镜像的顶层是不同,从而无法复用。

云应用

云平台可以让你在无需担心服务器配置的情况下创建并启动新的项目,可以削减成本并降低管理服务器的风险,此外,利用无论何时需要都可以添加额外实例的能力,可以轻松实现伸缩

在你开始开发程序,并规划在未来移植到云平台,会有许多需要考虑的体系架构和设计问题。这里着重阐述构建基于云的应用程序时需要注意的一些事项。

垂直伸缩与水平伸缩

在你优化应用程序时,最初的反应通常是进行垂直伸缩,比如添加更多的 RAM,增加处理器速度,升级到更快的硬盘,为较慢的查询或常用计算实现缓存。虽然这些都是合理的技术,但是,如果你的应用程序超出了某个特定的点,则不能再对其进行垂直伸缩,只能跨多台服务器对其进行水平伸缩。这样做往往需要对应用程序的体系架构进行重大改动。云应用程序的性质意味着你应该从开始就构建水平伸缩。

此外,在你构建云应用程序时,不应将重点放在较小的性能优化上。最好能够保持设计简单并确保你可以水平伸缩应用程序,而不是进行许多复杂的缓存和性能微调。服务器通常比开发人员价格便宜,而较小的性能优化常常不值得开发人员浪费时间,并且会降低应用程序的可维护性。在考虑对应用程序进行水平伸缩时,请将重点放在五种架构方法上:

  • 最大限度地减少可变状态。
  • 评估 NoSQL 数据存储。
  • 创建异步服务。
  • 自动部署。
  • 故障设计。

最大限度地减少可变状态

在云中增加水平可伸缩性的最重要模式就是最大限度地减少可变状态。这也是 ”函数式编程语言“ 突然流行,并且像 ”事件“ 这样的模式被更频繁地应用在互联网系统中的原因。共享可变状态(也就是说,在整个应用程序中共享会随时间变化的变量)的问题在于它对可伸缩性起到了破坏作用。例如,尝试同时升级同一变量的多个服务器和进程可能会导致死锁、超时以及事务失败。你能够最大限度地减少或消除服务器中的可变状态的三个位置是:Web 服务器、应用程序以及(甚至可能)数据库中。

不变的文件系统

要记住,在云平台上任何实例都可能随时消失。如果服务器实例消失导致文件丢失可以接受,那么你只需在本地文件系统上存储信息,否则,当用户正在上传文件(无论个人图片或报表文件),你需要确保将它们上传到冗余的、远程的文件系统上,而不只是将它们上传到用户正在访问的 Web 服务器的硬盘上。

不变的应用程序

虽然共享可变状态特别适合面向对象的编程,但是很重要的一点是,在伸缩基于云的应用程序时,需要最大限度地减少可变状态的数量并仔细考虑所产生的影响。

在适用于最小化可变状态的领域驱动设计(Domain Driven Design)中,Eric Evans引入了一些良好的代码模式,如尽可能少地使用可变实体,对应用程序状态尽可能多的使用不可变值对象。例如,用户是可变实体,但是你可以使用不可变值对象来表示用户属性(如地址),如果用户移动了位置,那么只需为该用户更改所指向的地址。一般情况下,可变状态下的偏向值对象 (favor value object) 和不变值尤其适用于需要定期通过基于云的应用程序的许多部件进行更新的状态。

不可变值对象实例一旦创建完成后,就不能改变其成员变量值。

此外,请认真考虑你的缓存策略。如果信息仅用于单一实例,那么本地缓存就没有问题,因为即使该实例停止工作,你始终可以重新创建它。但是在因为用户访问另一个 Web 服务器而需要信息时,你要么需要每次都将该信息从共享的永久存储器中取出,要么需要使用分布式缓存,跨多个 Web 服务器实例处理缓存更新。

不变的数据存储

虽然起初听起来像是有点矛盾,但是采用影响事件来源的方法来设计适用于基于云的应用程序的数据库,可能带来一些真正的好处。在你尝试伸缩应用程序时,常遇见的问题是数据库中的写入争用。例如,更改采购订单,可能会影响数以百计的订单项和供应商的相关采购订单。当然,如果只更改某个子集,任何这样的更改都必须包含在一项事务中,以确保数据库从未处于无效状态下。但是由于越来越多的用户更新了影响相关订单的事务,所以很快你就会了解到:因为乐观锁定冲突,所有的事务都失败了。

此问题的一种解决方法是要最大限度地减少数据库中的可变状态。在数据库中,用户信息只会包含有唯一标识符的单一记录,而不包含使用可变名称、姓氏和其他个人资料字段。然后,你会有适用于资料更新事件ProfileUpdateEvent的单独表,表中使用相关的时间戳为每次更新存储了一个不变事件。因此,如果用户更新了他所在的城市,那么将会发生 ProfileUpdateEvent事件,其中包含字段home_city和新的值,并存储了更新的日期/时间。若要获得用户所在的城市,只需浏览所有的ProfileUpdateEvent,直到发现更新该字段的最新时间戳为止。此过程会为你提供一个没有写入冲突的系统,将解决读取或自动化任务上的任何冲突,同时,在应用程序中,允许使用更大的写入可伸缩性。

user_profile数据存储设计1(单一记录):

|name    |age    |home_city|
|Xiao    |18     |Hangzhou |
|Wang    |23     |Beijing  |

//存在分布式写入冲突和锁等待

user_profile数据存储设计2(基于事件):

|name    |age    |home_city|event    |update_time     |
|Wang    |23     |Beijing  |add      |2017/10/31 10:28|
|Xiao    |18     |Hangzhou |add      |2017/10/31 12:31|
|Xiao    |888    |Hangzhou |update   |2017/11/01 01:03|
|Xiao    |888    |Hangzhou |delete   |2017/11/11 09:25|

//没有写入和读取冲突

当然,你现在还存在读取性能的问题。为了获得单一实体的状态,必须在非常大的事件集合上执行操作,这样做的效率不是很高,对于应用程序的读取可伸缩性来说,这是一个问题。但是,这是一个容易解决的问题:在用户表中重新创建其他字段,这些字段只是特定时间点上的用户状态的缓存。然后,你可以根据你的业务规则编写脚本,用于更新基于ProfileUpdateEvent的用户表中的状态缓存,并解决任何基于业务规则的写入冲突。如果写入冲突可能成为伸缩云应用的障碍,那么 ”事件模式“ 就是值得考虑的事项。

评估 NoSQL 数据存储

另一种管理数据库中写入冲突的方法是,为一些或所有应用程序数据评估使用 NoSQL 数据存储的可能性。例如,Cassandra 专门用来为存储大量数据提供跨多个节点的线性写入可伸缩性。CouchDB 提供了跨多个节点的 “主机到主机” 同步,MongoDB 则具有 “计数器” 字段的概念 { $inc: { <field1>: <amount1>, <field2>: <amount2>, ... } },如果你只是定期更新某个发布上的用户观点数量,那么应该将 “发布后忘记 (fire-and-forget) ” 的更新操作同步到计数器字段, 以使你不必担心写入冲突。

许多 NoSQL 存储还提供 MapReduce 功能,以便允许有效地访问预定义的查询。如果你正对已知的大数据集进行查询,那么由于数据量正在增加,MapReduce 可能会提供一种可伸缩的解决方案。

创建异步服务

另一种重要的体系架构方法是:通过创建单独的异步服务,减轻你的主应用程序服务器工作负载。在等待同步任务时,无需阻碍某个线程,也无需使你的站点访问者等待。将这些类型的任务划分为可在云中不同服务器上运行的独立服务会更好一些。这样,你就可以独立地从主应用程序伸缩它们。在等待任务完成时,无需阻碍 Web 服务器上的线程,同时你的用户无需等待这些过程完成后才获得响应,用户会得到服务器正在工作的消息,并会在该过程完成时得到通知。

使用具有某种程度交付保障能力的传输机制(如消息队列),你通常会连接到异步服务。使用网络而不是进程间通信也是很重要的。一般情况下,请在独立的服务器上运行这些服务。你还可以使用其他体系架构方法,比如将数据库当做黑盒用,以共享主应用程序以及执行独立异步服务的各种服务之间的信息。

自动部署

自动化云部署过程是很重要的。你需要为各种角色(Web服务器、电子邮件服务器、数据库服务器等)设置基本机器镜像,并使用工具自动配置实例、自动部署代码。

对基于云的应用程序的监控也非常重要。设置监控以了解实例何时停止或超载,这样就可以根据应用程序的执行情况自动地生成更多或更少的实例。

一定要有一个自动化的、脚本化的IP地址重映射工具来处理数据中心崩溃时的情况。

故障设计

对于基于云的应用程序来说,故障设计尤其重要。你应该仔细想想各种故障的场景,思考你的故障点和故障的时候你会怎么做。

然而,同样重要的是决定什么程度的失败是可以接受的。如果一个数据中心崩溃了,你只能在几分钟内为你的服务器提供读访问权限,直到你把另一个数据库升级为“主”,也许这种失败是可以接受的;如果服务器失败,用户将不得不再次登录,或者丢失自己的首选项或购物车,也许这种失败是可以接受的。将某失败容忍度的工程成本与此容忍度下的商业价值进行比较,确保你不会将过多的资金投资在对你的业务不重要的冗余信息上,这一点很重要。

文件大小写

不同的操作系统对于文件名的处理方式不同,而比较大的一个差异在于是否区分文件名的英文大小写,Windows不区分,而Linux和BSD则区分,MacOS需要取决于使用的文件系统。虽然Windows系统下不会区分文件名的大小写,但当文件被复制到其他区分文件名大小写的操作系统时,则会以当前文件命名时的状态被复制。

目录结构

Linux等操作系统使用的是单根节点的文件树,目录之间的分割为“/”符号,而Windows则使用了磁盘分区的概念,同时目录之间的分割符号为“\”符号。一般可以通过使用相对路径来避免处理根的问题,或使用java.io.File类中的File.separator获得路径分隔符。

results matching ""

    No results matching ""