关于 Spring For All

关于 Spring For All

Spring For All 的一切
最新动态

最新动态

Spring 5 会是咋样呢
Spring Boot

Spring Boot

快速构建并运行 Spring 应用程序
Spring Cloud

Spring Cloud

分布式系统的一套工具,可用于构建微服务
Spring Framework

Spring Framework

提供依赖注入、事务、Web应用、数据访问等模块
Spring Data

Spring Data

提供一致性数据访问模块
Spring Security

Spring Security

提供应用身份验证和授权支持
Spring Batch

Spring Batch

提供高容批处理操作模块
Spring AMQP

Spring AMQP

基于AMQP消息解决方案
Micro Service Arch.

Micro Service Arch.

微服务架构相关
开源项目及视频教程

开源项目及视频教程

做中国最好的 Spring 开源项目及视频教程
小马哥专栏

小马哥专栏

阿里技术布道者,专注 Spring Boot 及微服务

每个微服务之间都是通过共享数据库,共享缓存进行交互的,并没采用彼此之间的接口调用。

采蘑菇的大叔 回复了问题 • 8 人关注 • 6 个回复 • 902 次浏览 • 2017-09-12 13:49 • 来自相关话题

服务端指南 | 良好的 API 设计指南

梁桂钊 发表了文章 • 6 个评论 • 4585 次浏览 • 2017-08-03 08:16 • 来自相关话题

版本号

在 RESTful API 中,API 接口应该尽量兼容之前的版本。但是,在实际业务开发场景中,可能随着业务需求的不断迭代,现有的 API 接口无法支持旧版本的适配,此时如果强制升级服务端的 API 接口将导致客户端旧有功能出现故障。实际上,Web 端是部署在服务器,因此它可以很容易为了适配服务端的新的 API 接口进行版本升级,然而像 Android 端、IOS 端、PC 端等其他客户端是运行在用户的机器上,因此当前产品很难做到适配新的服务端的 API 接口,从而出现功能故障,这种情况下,用户必须升级产品到最新的版本才能正常使用。

为了解决这个版本不兼容问题,在设计 RESTful API 的一种实用的做法是使用版本号。一般情况下,我们会在 url 中保留版本号,并同时兼容多个版本。$(document).ready(function() {$('pre code').each(function(i, block) { hljs.highlightBlock( block); }); });【GET】 /v1/users/{user_id} // 版本 v1 的查询用户列表的 API 接口
【GET】 /v2/users/{user_id} // 版本 v2 的查询用户列表的 API 接口
现在,我们可以不改变版本 v1 的查询用户列表的 API 接口的情况下,新增版本 v2 的查询用户列表的 API 接口以满足新的业务需求,此时,客户端的产品的新功能将请求新的服务端的 API 接口地址。虽然服务端会同时兼容多个版本,但是同时维护太多版本对于服务端而言是个不小的负担,因为服务端要维护多套代码。这种情况下,常见的做法不是维护所有的兼容版本,而是只维护最新的几个兼容版本,例如维护最新的三个兼容版本。在一段时间后,当绝大多数用户升级到较新的版本后,废弃一些使用量较少的服务端的老版本API 接口版本,并要求使用产品的非常旧的版本的用户强制升级。

注意的是,“不改变版本 v1 的查询用户列表的 API 接口”主要指的是对于客户端的调用者而言它看起来是没有改变。而实际上,如果业务变化太大,服务端的开发人员需要对旧版本的 API 接口使用适配器模式将请求适配到新的API 接口上。

资源路径

RESTful API 的设计以资源为核心,每一个 URI 代表一种资源。因此,URI 不能包含动词,只能是名词。注意的是,形容词也是可以使用的,但是尽量少用。一般来说,不论资源是单个还是多个,API 的名词要以复数进行命名。此外,命名名词的时候,要使用小写、数字及下划线来区分多个单词。这样的设计是为了与 json 对象及属性的命名方案保持一致。例如,一个查询系统标签的接口可以进行如下设计。【GET】 /v1/tags/{tag_id}
同时,资源的路径应该从根到子依次如下。/{resources}/{resource_id}/{sub_resources}/{sub_resource_id}/{sub_resource_property}
我们来看一个“添加用户的角色”的设计,其中“用户”是主资源,“角色”是子资源。【POST】 /v1/users/{user_id}/roles/{role_id} // 添加用户的角色
 有的时候,当一个资源变化难以使用标准的 RESTful API 来命名,可以考虑使用一些特殊的 actions 命名。/{resources}/{resource_id}/actions/{action}
举个例子,“密码修改”这个接口的命名很难完全使用名词来构建路径,此时可以引入 action 命名。【PUT】 /v1/users/{user_id}/password/actions/modify // 密码修改
请求方式

可以通过 GET、 POST、 PUT、 PATCH、 DELETE 等方式对服务端的资源进行操作。其中,GET 用于查询资源,POST 用于创建资源,PUT 用于更新服务端的资源的全部信息,PATCH 用于更新服务端的资源的部分信息,DELETE 用于删除服务端的资源。

这里,笔者使用“用户”的案例进行回顾通过 GET、 POST、 PUT、 PATCH、 DELETE 等方式对服务端的资源进行操作。【GET】 /users # 查询用户信息列表
【GET】 /users/1001 # 查看某个用户信息
【POST】 /users # 新建用户信息
【PUT】 /users/1001 # 更新用户信息(全部字段)
【PATCH】 /users/1001 # 更新用户信息(部分字段)
【DELETE】 /users/1001 # 删除用户信息
查询参数

RESTful API 接口应该提供参数,过滤返回结果。其中,offset 指定返回记录的开始位置。一般情况下,它会结合 limit 来做分页的查询,这里 limit 指定返回记录的数量。【GET】 /{version}/{resources}/{resource_id}?offset=0&limit=20
同时,orderby 可以用来排序,但仅支持单个字符的排序,如果存在多个字段排序,需要业务中扩展其他参数进行支持。【GET】 /{version}/{resources}/{resource_id}?orderby={field} [asc|desc]
为了更好地选择是否支持查询总数,我们可以使用 count 字段,count 表示返回数据是否包含总条数,它的默认值为 false。【GET】 /{version}/{resources}/{resource_id}?count=[true|false]
上面介绍的 offset、 limit、 orderby 是一些公共参数。此外,业务场景中还存在许多个性化的参数。我们来看一个例子。【GET】 /v1/categorys/{category_id}/apps/{app_id}?enable=[1|0]&os_type={field}&device_ids={field,field,…}
注意的是,不要过度设计,只返回用户需要的查询参数。此外,需要考虑是否对查询参数创建数据库索引以提高查询性能。

状态码

使用适合的状态码很重要,而不应该全部都返回状态码 200,或者随便乱使用。这里,列举笔者在实际开发过程中常用的一些状态码,以供参考。状态码 描述
200 请求成功
201 创建成功
400 错误的请求
401 未验证
403 被拒绝
404 无法找到
409 资源冲突
500 服务器内部错误
异常响应

当 RESTful API 接口出现非 2xx 的 HTTP 错误码响应时,采用全局的异常结构响应信息。HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"code": "INVALID_ARGUMENT",
"message": "{error message}",
"cause": "{cause message}",
"request_id": "01234567-89ab-cdef-0123-456789abcdef",
"host_id": "{server identity}",
"server_time": "2014-01-01T12:00:00Z"
}
请求参数

在设计服务端的 RESTful API 的时候,我们还需要对请求参数进行限制说明。例如一个支持批量查询的接口,我们要考虑最大支持查询的数量。【GET】 /v1/users/batch?user_ids=1001,1002 // 批量查询用户信息
参数说明
- user_ids: 用户ID串,最多允许 20 个。
此外,在设计新增或修改接口时,我们还需要在文档中明确告诉调用者哪些参数是必填项,哪些是选填项,以及它们的边界值的限制。
 【POST】 /v1/users // 创建用户信息
请求内容
{
"username": "lgz", // 必填, 用户名称, max 10
"realname": "梁桂钊", // 必填, 用户名称, max 10
"password": "123456", // 必填, 用户密码, max 32
"email": "lianggzone@163.com", // 选填, 电子邮箱, max 32
"weixin": "LiangGzone", // 选填,微信账号, max 32
"sex": 1 // 必填, 用户性别[1-男 2-女 99-未知]
}
响应参数

针对不同操作,服务端向用户返回的结果应该符合以下规范。【GET】 /{version}/{resources}/{resource_id} // 返回单个资源对象
【GET】 /{version}/{resources} // 返回资源对象的列表
【POST】 /{version}/{resources} // 返回新生成的资源对象
【PUT】 /{version}/{resources}/{resource_id} // 返回完整的资源对象
【PATCH】 /{version}/{resources}/{resource_id} // 返回完整的资源对象
【DELETE】 /{version}/{resources}/{resource_id} // 状态码 200,返回完整的资源对象。
// 状态码 204,返回一个空文档
如果是单条数据,则返回一个对象的 JSON 字符串。
 
HTTP/1.1 200 OK
{
"id" : "01234567-89ab-cdef-0123-456789abcdef",
"name" : "example",
"created_time": 1496676420000,
"updated_time": 1496676420000,
...
}
如果是列表数据,则返回一个封装的结构体。
 
HTTP/1.1 200 OK
{
"count":100,
"items":[
{
"id" : "01234567-89ab-cdef-0123-456789abcdef",
"name" : "example",
"created_time": 1496676420000,
"updated_time": 1496676420000,
...
},
...
]
}


一个完整的案例

最后,我们使用一个完整的案例将前面介绍的知识整合起来。这里,使用“获取用户列表”的案例。【GET】 /v1/users?[&keyword=xxx][&enable=1][&offset=0][&limit=20] 获取用户列表
功能说明:获取用户列表
请求方式:GET
参数说明
- keyword: 模糊查找的关键字。[选填]
- enable: 启用状态[1-启用 2-禁用]。[选填]
- offset: 获取位置偏移,从 0 开始。[选填]
- limit: 每次获取返回的条数,缺省为 20 条,最大不超过 100。 [选填]
响应内容
HTTP/1.1 200 OK
{
"count":100,
"items":[
{
"id" : "01234567-89ab-cdef-0123-456789abcdef",
"name" : "example",
"created_time": 1496676420000,
"updated_time": 1496676420000,
...
},
...
]
}
失败响应
HTTP/1.1 403 UC/AUTH_DENIED
Content-Type: application/json
{
"code": "INVALID_ARGUMENT",
"message": "{error message}",
"cause": "{cause message}",
"request_id": "01234567-89ab-cdef-0123-456789abcdef",
"host_id": "{server identity}",
"server_time": "2014-01-01T12:00:00Z"
}
错误代码
- 403 UC/AUTH_DENIED 授权受限 查看全部
版本号

在 RESTful API 中,API 接口应该尽量兼容之前的版本。但是,在实际业务开发场景中,可能随着业务需求的不断迭代,现有的 API 接口无法支持旧版本的适配,此时如果强制升级服务端的 API 接口将导致客户端旧有功能出现故障。实际上,Web 端是部署在服务器,因此它可以很容易为了适配服务端的新的 API 接口进行版本升级,然而像 Android 端、IOS 端、PC 端等其他客户端是运行在用户的机器上,因此当前产品很难做到适配新的服务端的 API 接口,从而出现功能故障,这种情况下,用户必须升级产品到最新的版本才能正常使用。

为了解决这个版本不兼容问题,在设计 RESTful API 的一种实用的做法是使用版本号。一般情况下,我们会在 url 中保留版本号,并同时兼容多个版本。
【GET】 /v1/users/{user_id} // 版本 v1 的查询用户列表的 API 接口
【GET】 /v2/users/{user_id} // 版本 v2 的查询用户列表的 API 接口

现在,我们可以不改变版本 v1 的查询用户列表的 API 接口的情况下,新增版本 v2 的查询用户列表的 API 接口以满足新的业务需求,此时,客户端的产品的新功能将请求新的服务端的 API 接口地址。虽然服务端会同时兼容多个版本,但是同时维护太多版本对于服务端而言是个不小的负担,因为服务端要维护多套代码。这种情况下,常见的做法不是维护所有的兼容版本,而是只维护最新的几个兼容版本,例如维护最新的三个兼容版本。在一段时间后,当绝大多数用户升级到较新的版本后,废弃一些使用量较少的服务端的老版本API 接口版本,并要求使用产品的非常旧的版本的用户强制升级。

注意的是,“不改变版本 v1 的查询用户列表的 API 接口”主要指的是对于客户端的调用者而言它看起来是没有改变。而实际上,如果业务变化太大,服务端的开发人员需要对旧版本的 API 接口使用适配器模式将请求适配到新的API 接口上。

资源路径

RESTful API 的设计以资源为核心,每一个 URI 代表一种资源。因此,URI 不能包含动词,只能是名词。注意的是,形容词也是可以使用的,但是尽量少用。一般来说,不论资源是单个还是多个,API 的名词要以复数进行命名。此外,命名名词的时候,要使用小写、数字及下划线来区分多个单词。这样的设计是为了与 json 对象及属性的命名方案保持一致。例如,一个查询系统标签的接口可以进行如下设计。
【GET】 /v1/tags/{tag_id}

同时,资源的路径应该从根到子依次如下。
/{resources}/{resource_id}/{sub_resources}/{sub_resource_id}/{sub_resource_property}

我们来看一个“添加用户的角色”的设计,其中“用户”是主资源,“角色”是子资源。
【POST】 /v1/users/{user_id}/roles/{role_id} // 添加用户的角色

 有的时候,当一个资源变化难以使用标准的 RESTful API 来命名,可以考虑使用一些特殊的 actions 命名。
/{resources}/{resource_id}/actions/{action}

举个例子,“密码修改”这个接口的命名很难完全使用名词来构建路径,此时可以引入 action 命名。
【PUT】 /v1/users/{user_id}/password/actions/modify // 密码修改

请求方式

可以通过 GET、 POST、 PUT、 PATCH、 DELETE 等方式对服务端的资源进行操作。其中,GET 用于查询资源,POST 用于创建资源,PUT 用于更新服务端的资源的全部信息,PATCH 用于更新服务端的资源的部分信息,DELETE 用于删除服务端的资源。

这里,笔者使用“用户”的案例进行回顾通过 GET、 POST、 PUT、 PATCH、 DELETE 等方式对服务端的资源进行操作。
【GET】 /users # 查询用户信息列表
【GET】 /users/1001 # 查看某个用户信息
【POST】 /users # 新建用户信息
【PUT】 /users/1001 # 更新用户信息(全部字段)
【PATCH】 /users/1001 # 更新用户信息(部分字段)
【DELETE】 /users/1001 # 删除用户信息

查询参数

RESTful API 接口应该提供参数,过滤返回结果。其中,offset 指定返回记录的开始位置。一般情况下,它会结合 limit 来做分页的查询,这里 limit 指定返回记录的数量。
【GET】 /{version}/{resources}/{resource_id}?offset=0&limit=20

同时,orderby 可以用来排序,但仅支持单个字符的排序,如果存在多个字段排序,需要业务中扩展其他参数进行支持。
【GET】 /{version}/{resources}/{resource_id}?orderby={field} [asc|desc]

为了更好地选择是否支持查询总数,我们可以使用 count 字段,count 表示返回数据是否包含总条数,它的默认值为 false。
【GET】 /{version}/{resources}/{resource_id}?count=[true|false]

上面介绍的 offset、 limit、 orderby 是一些公共参数。此外,业务场景中还存在许多个性化的参数。我们来看一个例子。
【GET】 /v1/categorys/{category_id}/apps/{app_id}?enable=[1|0]&os_type={field}&device_ids={field,field,…}

注意的是,不要过度设计,只返回用户需要的查询参数。此外,需要考虑是否对查询参数创建数据库索引以提高查询性能。

状态码

使用适合的状态码很重要,而不应该全部都返回状态码 200,或者随便乱使用。这里,列举笔者在实际开发过程中常用的一些状态码,以供参考。
状态码 描述
200 请求成功
201 创建成功
400 错误的请求
401 未验证
403 被拒绝
404 无法找到
409 资源冲突
500 服务器内部错误

异常响应

当 RESTful API 接口出现非 2xx 的 HTTP 错误码响应时,采用全局的异常结构响应信息。
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"code": "INVALID_ARGUMENT",
"message": "{error message}",
"cause": "{cause message}",
"request_id": "01234567-89ab-cdef-0123-456789abcdef",
"host_id": "{server identity}",
"server_time": "2014-01-01T12:00:00Z"
}

请求参数

在设计服务端的 RESTful API 的时候,我们还需要对请求参数进行限制说明。例如一个支持批量查询的接口,我们要考虑最大支持查询的数量。
【GET】 /v1/users/batch?user_ids=1001,1002 // 批量查询用户信息
参数说明
- user_ids: 用户ID串,最多允许 20 个。

此外,在设计新增或修改接口时,我们还需要在文档中明确告诉调用者哪些参数是必填项,哪些是选填项,以及它们的边界值的限制。
 
【POST】     /v1/users                             // 创建用户信息
请求内容
{
"username": "lgz", // 必填, 用户名称, max 10
"realname": "梁桂钊", // 必填, 用户名称, max 10
"password": "123456", // 必填, 用户密码, max 32
"email": "lianggzone@163.com", // 选填, 电子邮箱, max 32
"weixin": "LiangGzone", // 选填,微信账号, max 32
"sex": 1 // 必填, 用户性别[1-男 2-女 99-未知]
}

响应参数

针对不同操作,服务端向用户返回的结果应该符合以下规范。
【GET】 /{version}/{resources}/{resource_id} // 返回单个资源对象
【GET】 /{version}/{resources} // 返回资源对象的列表
【POST】 /{version}/{resources} // 返回新生成的资源对象
【PUT】 /{version}/{resources}/{resource_id} // 返回完整的资源对象
【PATCH】 /{version}/{resources}/{resource_id} // 返回完整的资源对象
【DELETE】 /{version}/{resources}/{resource_id} // 状态码 200,返回完整的资源对象。
// 状态码 204,返回一个空文档

如果是单条数据,则返回一个对象的 JSON 字符串。
 
HTTP/1.1 200 OK
{
"id" : "01234567-89ab-cdef-0123-456789abcdef",
"name" : "example",
"created_time": 1496676420000,
"updated_time": 1496676420000,
...
}

如果是列表数据,则返回一个封装的结构体。
 
HTTP/1.1 200 OK
{
"count":100,
"items":[
{
"id" : "01234567-89ab-cdef-0123-456789abcdef",
"name" : "example",
"created_time": 1496676420000,
"updated_time": 1496676420000,
...
},
...
]
}


一个完整的案例

最后,我们使用一个完整的案例将前面介绍的知识整合起来。这里,使用“获取用户列表”的案例。
【GET】     /v1/users?[&keyword=xxx][&enable=1][&offset=0][&limit=20] 获取用户列表
功能说明:获取用户列表
请求方式:GET
参数说明
- keyword: 模糊查找的关键字。[选填]
- enable: 启用状态[1-启用 2-禁用]。[选填]
- offset: 获取位置偏移,从 0 开始。[选填]
- limit: 每次获取返回的条数,缺省为 20 条,最大不超过 100。 [选填]
响应内容
HTTP/1.1 200 OK
{
"count":100,
"items":[
{
"id" : "01234567-89ab-cdef-0123-456789abcdef",
"name" : "example",
"created_time": 1496676420000,
"updated_time": 1496676420000,
...
},
...
]
}
失败响应
HTTP/1.1 403 UC/AUTH_DENIED
Content-Type: application/json
{
"code": "INVALID_ARGUMENT",
"message": "{error message}",
"cause": "{cause message}",
"request_id": "01234567-89ab-cdef-0123-456789abcdef",
"host_id": "{server identity}",
"server_time": "2014-01-01T12:00:00Z"
}
错误代码
- 403 UC/AUTH_DENIED 授权受限

HTTPS 项目实战指南

梁桂钊 发表了文章 • 3 个评论 • 380 次浏览 • 2017-06-27 06:45 • 来自相关话题

本文将重点介绍关于 HTTPS 的几个实战指南。

HTTPS 使用剖析
HTTPS 项目场景
HTTPS 设计上的借鉴
HTTPS 降级攻击

HTTPS 使用剖析与项目场景

HTTP 协议没有加密机制,可以通过 SSL 或 TLS 加密 HTTP 的通信内容。因此,HTTPS 是 HTTP 的安全版,在 HTTP 协议中加入 SSL 层,它由两部分组成:HTTP 与 SSL。其中,SSL 是独立于 HTTP 的协议,它不仅可以适用于 HTTP 协议,还可以配合 WebSocket 等协议一起使用。

为什么使用 HTTPS

HTTP 协议没有加密机制,通信内容是明文传输的,没有经过任何安全处理。然而,互联网的任何角落都可能存在通信内容在传输过程中被中间者窃听、篡改、冒充等风险。其中,任何角落指的是用户的通信内容在浏览器和服务器中间传输必须要经过的节点,比如 WIFI 热点,路由器,防火墙,反向代理,缓存服务器等。因此,HTTP 协议通信存在的威胁不言而喻,中间者可以窃听隐私,使用户的敏感信息泄露;篡改网页,例如中间者向页面插入广告内容,甚至有的中间者进行流量劫持,例如有的时候会发现域名没有输错,结果却跳转到了一个钓鱼网站,因为这个网站被中间者劫持了。

因此,为了解决窃听、篡改、冒充等风险,HTTPS 的价值就体现出来了,HTTPS 可以进行通信内容加密,使第三方无法窃听。HTTPS 可以进行身份认证,一旦通信内容被篡改,通信双方会立刻发现。此外,HTTPS 可以保证通信内容的完整性,防止通信内容冒充或者篡改。

SSL 与 TLS

HTTP 协议可以通过 SSL 或 TLS 加密 HTTP 的通信内容。其中,SSL 最初由网景通信公司率先倡导与开发,最新版本是 SSL 3.0。目前,由 IETF 主导与管理。IETF 以 SSL 3.0 为基准,在 SSL 3.0 协议规范之上又制定了 TLS 1.0、 TLS 1.1 和 TLS 1.2。

HTTPS 原理剖析

为了更好地理解 HTTPS,先来观察一下 HTTPS 的通信步骤。






第一步,用户在浏览器里输入一个支持 HTTPS 协议的网址,例如 https://blog.720ui.com,此时会发起一个 HTTPS 请求,通过 TCP 和服务器建立连接,其中使用 443 端口。

第二步,服务器向客户端发送证书,其中包括域名,申请证书的公司,证书的公钥等信息。

第三步,客户端通过 SSL 或 TLS 对证书进行解析,验证公钥是否有效,例如验证颁发机构与验证过期时间等信息,如果发现证书异常,则会弹出一个警告框,提示证书存在问题。如果证书没有问题,那么就生成一个密钥,然后向服务器发送证书公钥加密后的密钥。

第四步,服务器用证书私钥进行解密,获得客户端传过来的密钥。

第五步,服务器用客户端的密钥加密后的信息发送给客户端。

第六步,客户端用密钥解密服务端传过来的信息,验证服务端传过来的信息是否可以用密钥进行解密。

第七步,客户端用密钥加密请求内容,然后将通信内容发送给服务端。

第六步,服务端用密钥解密请求内容,并进行业务处理后用密钥加密响应内容,然后将通信内容发送给客户端。

在以上流程中,HTTPS 协议将对称加密算法与非对称加密算法的优势相结合。使用对称加密算法对通信内容进行快速加密,从而弥补了非对称加密算法处理速度慢的问题,并保证通信内容的机密性。同时,使用非对称加密算法将对称加密算法的密钥进行加密,保证对称加密算法的密钥的安全交换。因此,HTTPS 协议先通过非对称加密算法进行密钥的安全交换,并在建立通信连接后,使用对称加密算法进行通信,保证通信内容的机密性。

HTTPS 项目场景

真实的业务场景是比较复杂的。这里,整理 3 个项目中遇到的比较复杂的应用场景。

对 HTTPS 协议的通信内容进行 CDN 加速

对 HTTPS 协议的通信内容进行 CDN 加速主要两个方案。

方案一,服务器(源站)提供证书给 CDN 厂商,包括证书公钥和证书私钥,CDN 负责内容缓存,CDN 有缓存则直接响应,以 HTTP 或 HTTPS 的形式回源。这个方案,适用仅对防劫持、防篡改有需求,而愿意提供证书给 CDN 的源站加速。

方案二,服务器(源站)不提供证书给 CDN 厂商,因此 CDN 需要存放证书公钥,服务器(源站)存放证书私钥。在 CDN 与浏览器进行 TLS 的认证和密钥协商过程中,通过安全的信道把协商过程中的信息以 HTTP 或 HTTPS 的形式转发给源网站。这个方案,CDN 不做缓存,仅以自有的加速网络,将用户的请求快速送到服务器(源站),降低公网延迟。因此,这个方案适用于对安全要求更高,不愿将证书私钥共享给 CDN 的源站加速。

对 HTTPS 的域名通过 CNAME 绑定到另外一个 HTTPS 域名上

对 HTTPS 的域名通过 CNAME 绑定到另外一个 HTTPS 域名上,这个情况下,需要两个证书,因为每个证书跟自己的域名进行绑定,即使它们都在同一个服务器上,也不能使用同一个证书。

两台服务器的证书问题






因为安全问题,CA 证书部署在一台服务器上面,但是业务系统部署在另外一台服务器上面,这种情况就比较难办。此时,需要借助 Nginx 进行反向代理,回源到具体的服务器。

HTTPS 设计上的借鉴

对称加密算法可以将敏感的信息在通信过程中通过 DES 或 AES 进行加密传输,然后在客户端和服务端直接进行解密。但是,将密钥编码在代码中存在安全隐患,因为密钥可能会泄漏。因此,更安全的做法是将密钥保存在数据库中,由服务端的接口下发密钥,在使用时由客户端获取密钥并加载进内存,并且通过非对称加密算法保证密钥在通信过程中的安全交换。实际上,这个通信流程可以借鉴 HTTPS 协议的流程,将对称加密算法与非对称加密算法的优势相结合。其中,使用对称加密算法对通信内容进行快速加密,从而弥补了非对称加密算法处理速度慢的问题,并保证通信内容的机密性。同时,使用非对称加密算法将对称加密算法的密钥进行加密,保证对称加密算法的密钥的安全交换。

这里,介绍一个文件加密与解密的真实案例。需求场景是为了防止平台资源被第三方抓取,因此需要对平台的资源进行加密,并且只能用于平台的客户端使用。为了更好地理解整个通信流程,先来观察一下加密的通信步骤。






第一步,客户端 SDK 使用 RSA 加密算法,生成一对公私钥,或者称之为加密密钥与解密密钥。

第二步,客户端 SDK 向服务端请求 AES 密钥,因为密钥保存在服务器的数据库中,由服务端的接口下发密钥。其中,请求参数包括资源 ID 与 RSA 加密算法的公钥。

第三步,服务端使用客户端 SDK请求参数中的资源 ID,生成一个 AES 密钥,并保存到数据库中。

第四步,服务端使用客户端 SDK请求参数中的 RSA 加密算法的公钥,加密AES 密钥。

第五步,服务器发送给客户端 SDK加密后的 AES 密钥。

第六步,客户端 SDK 使用RSA 加密算法的私钥解密,获取到真正的 AES 密钥。

第七步,客户端 SDK 使用真正的 AES 密钥对资源文件进行加密。

理解了资源文件的加密的通信流程,再来观察一下解密的通信步骤。






与资源文件的加密的通信流程类似,客户端 SDK 使用 RSA 加密算法,生成一对公私钥,客户端 SDK 向服务端请求 AES 密钥,服务端使用客户端 SDK请求参数中的资源 ID 查询 AES 密钥,服务端使用客户端 SDK请求参数中的 RSA 加密算法的公钥,加密AES 密钥,并且服务器发送给客户端 SDK加密后的 AES 密钥,客户端 SDK 使用RSA 加密算法的私钥解密,获取到真正的 AES 密钥,最后,客户端 SDK 使用真正的 AES 密钥对资源文件进行解密。

HTTPS 降级攻击的场景剖析与解决之道
HTTPS 一定安全么

HTTP 协议没有加密机制,通信内容是明文传输的,没有经过任何安全处理。因此,为了解决窃听、篡改、冒充等风险,采用 HTTPS 协议。HTTPS 可以进行通信内容加密,使第三方无法窃听。HTTPS 可以进行身份认证,一旦通信内容被篡改,通信双方会立刻发现。此外,HTTPS 可以保证通信内容的完整性,防止通信内容冒充或者篡改。那么,使用了 HTTPS 就能确保通信内容的安全传输吗?理论上,是这样的,但是事实上,现实却不是如此,因为设计与实现 SSL/TLS 协议出现了漏洞,导致攻击者同样可以攻击一些旧版本的 SSL/TLS 协议。这其中就包括 SSL 3.0。

什么是 HTTPS 降级攻击

因为浏览器的兼容性问题,当浏览器进行 HTTPS 连接失败的时候,将会尝试使用旧的协议版本,于是加密协议由更加安全的协议,比如 TLS 1.2 降级成 SSL 3.0。因此,在 HTTPS 降级攻击中,攻击者利用旧版本的 SSL/TLS 协议的漏洞,其中包括 SSL 3.0 的漏洞,然后攻击者获取安全连接当中某些可以降级成 SSL 3.0 的加密后的明文的通信内容进行攻击。

注意的是,如果服务器支持 SSL 3.0 协议,同时,攻击者又能作为中间人控制浏览器发起漏洞版本的 HTTPS 请求,此时,虽然攻击者窃听到加密的通信内容,但加密的协议存在漏洞,可以进行降级攻击,解密这些加密的通信内容可以获取有价值的信息,例如用户的隐私数据。

HTTPS 降级攻击的解决之道

目前,解决 HTTPS 降级攻击的唯一方法是禁用 SSL 3.0 协议,并防止 TLS 1.2 协议、 TLS 1.1 协议与 TLS 1.0 协议降级到 SSL 3.0 协议。其中,禁用 SSL 3.0 协议的策略有很多,这里主要介绍下 Nginx 如何防止 TLS 1.2 协议、 TLS 1.1 协议与 TLS 1.0 协议降级到 SSL 3.0 协议以下版本,从而防止 HTTPS 降级攻击。

Nginx 原先的配置,如下所示。此时,如果 Nginx 原先的配置没有额外的配置,那么在 Nginx 中隐性默认是 SSLv3 TLSv1 TLSv1.1 TLSv1.2。ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;
Nginx 禁用 SSL 3.0 协议,并防止 TLS 1.2 协议、 TLS 1.1 协议与 TLS 1.0 协议降级到 SSL 3.0 协议的配置非常简单,只需要屏蔽 SSLv3 参数即可,如下所示。ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
总结下,HTTPS 能确保通信内容的安全传输吗?答案是不一定,因为设计与实现 SSL/TLS 协议出现了漏洞,导致攻击者同样可以攻击一些旧版本的 SSL/TLS 协议,其中就包括 SSL 3.0,可能会被攻击者进行 HTTPS 的降级攻击。因此,SSL 3.0 协议并不安全,为了防止 HTTPS 的降级攻击,需要禁用 SSL 3.0 协议,确保TLS 1.2 协议、 TLS 1.1 协议与 TLS 1.0 协议降级到 SSL 3.0 协议以下版本。
 
(完)
 

本文作者:梁桂钊
原文链接:http://blog.720ui.com/2017/https_action/
版权归作者所有,转载请注明出处 查看全部
本文将重点介绍关于 HTTPS 的几个实战指南。

HTTPS 使用剖析
HTTPS 项目场景
HTTPS 设计上的借鉴
HTTPS 降级攻击

HTTPS 使用剖析与项目场景

HTTP 协议没有加密机制,可以通过 SSL 或 TLS 加密 HTTP 的通信内容。因此,HTTPS 是 HTTP 的安全版,在 HTTP 协议中加入 SSL 层,它由两部分组成:HTTP 与 SSL。其中,SSL 是独立于 HTTP 的协议,它不仅可以适用于 HTTP 协议,还可以配合 WebSocket 等协议一起使用。

为什么使用 HTTPS

HTTP 协议没有加密机制,通信内容是明文传输的,没有经过任何安全处理。然而,互联网的任何角落都可能存在通信内容在传输过程中被中间者窃听、篡改、冒充等风险。其中,任何角落指的是用户的通信内容在浏览器和服务器中间传输必须要经过的节点,比如 WIFI 热点,路由器,防火墙,反向代理,缓存服务器等。因此,HTTP 协议通信存在的威胁不言而喻,中间者可以窃听隐私,使用户的敏感信息泄露;篡改网页,例如中间者向页面插入广告内容,甚至有的中间者进行流量劫持,例如有的时候会发现域名没有输错,结果却跳转到了一个钓鱼网站,因为这个网站被中间者劫持了。

因此,为了解决窃听、篡改、冒充等风险,HTTPS 的价值就体现出来了,HTTPS 可以进行通信内容加密,使第三方无法窃听。HTTPS 可以进行身份认证,一旦通信内容被篡改,通信双方会立刻发现。此外,HTTPS 可以保证通信内容的完整性,防止通信内容冒充或者篡改。

SSL 与 TLS

HTTP 协议可以通过 SSL 或 TLS 加密 HTTP 的通信内容。其中,SSL 最初由网景通信公司率先倡导与开发,最新版本是 SSL 3.0。目前,由 IETF 主导与管理。IETF 以 SSL 3.0 为基准,在 SSL 3.0 协议规范之上又制定了 TLS 1.0、 TLS 1.1 和 TLS 1.2。

HTTPS 原理剖析

为了更好地理解 HTTPS,先来观察一下 HTTPS 的通信步骤。

HTTPS_的通信步骤.png


第一步,用户在浏览器里输入一个支持 HTTPS 协议的网址,例如 https://blog.720ui.com,此时会发起一个 HTTPS 请求,通过 TCP 和服务器建立连接,其中使用 443 端口。

第二步,服务器向客户端发送证书,其中包括域名,申请证书的公司,证书的公钥等信息。

第三步,客户端通过 SSL 或 TLS 对证书进行解析,验证公钥是否有效,例如验证颁发机构与验证过期时间等信息,如果发现证书异常,则会弹出一个警告框,提示证书存在问题。如果证书没有问题,那么就生成一个密钥,然后向服务器发送证书公钥加密后的密钥。

第四步,服务器用证书私钥进行解密,获得客户端传过来的密钥。

第五步,服务器用客户端的密钥加密后的信息发送给客户端。

第六步,客户端用密钥解密服务端传过来的信息,验证服务端传过来的信息是否可以用密钥进行解密。

第七步,客户端用密钥加密请求内容,然后将通信内容发送给服务端。

第六步,服务端用密钥解密请求内容,并进行业务处理后用密钥加密响应内容,然后将通信内容发送给客户端。

在以上流程中,HTTPS 协议将对称加密算法与非对称加密算法的优势相结合。使用对称加密算法对通信内容进行快速加密,从而弥补了非对称加密算法处理速度慢的问题,并保证通信内容的机密性。同时,使用非对称加密算法将对称加密算法的密钥进行加密,保证对称加密算法的密钥的安全交换。因此,HTTPS 协议先通过非对称加密算法进行密钥的安全交换,并在建立通信连接后,使用对称加密算法进行通信,保证通信内容的机密性。

HTTPS 项目场景

真实的业务场景是比较复杂的。这里,整理 3 个项目中遇到的比较复杂的应用场景。

对 HTTPS 协议的通信内容进行 CDN 加速

对 HTTPS 协议的通信内容进行 CDN 加速主要两个方案。

方案一,服务器(源站)提供证书给 CDN 厂商,包括证书公钥和证书私钥,CDN 负责内容缓存,CDN 有缓存则直接响应,以 HTTP 或 HTTPS 的形式回源。这个方案,适用仅对防劫持、防篡改有需求,而愿意提供证书给 CDN 的源站加速。

方案二,服务器(源站)不提供证书给 CDN 厂商,因此 CDN 需要存放证书公钥,服务器(源站)存放证书私钥。在 CDN 与浏览器进行 TLS 的认证和密钥协商过程中,通过安全的信道把协商过程中的信息以 HTTP 或 HTTPS 的形式转发给源网站。这个方案,CDN 不做缓存,仅以自有的加速网络,将用户的请求快速送到服务器(源站),降低公网延迟。因此,这个方案适用于对安全要求更高,不愿将证书私钥共享给 CDN 的源站加速。

对 HTTPS 的域名通过 CNAME 绑定到另外一个 HTTPS 域名上

对 HTTPS 的域名通过 CNAME 绑定到另外一个 HTTPS 域名上,这个情况下,需要两个证书,因为每个证书跟自己的域名进行绑定,即使它们都在同一个服务器上,也不能使用同一个证书。

两台服务器的证书问题

两台服务器的证书问题.png


因为安全问题,CA 证书部署在一台服务器上面,但是业务系统部署在另外一台服务器上面,这种情况就比较难办。此时,需要借助 Nginx 进行反向代理,回源到具体的服务器。

HTTPS 设计上的借鉴

对称加密算法可以将敏感的信息在通信过程中通过 DES 或 AES 进行加密传输,然后在客户端和服务端直接进行解密。但是,将密钥编码在代码中存在安全隐患,因为密钥可能会泄漏。因此,更安全的做法是将密钥保存在数据库中,由服务端的接口下发密钥,在使用时由客户端获取密钥并加载进内存,并且通过非对称加密算法保证密钥在通信过程中的安全交换。实际上,这个通信流程可以借鉴 HTTPS 协议的流程,将对称加密算法与非对称加密算法的优势相结合。其中,使用对称加密算法对通信内容进行快速加密,从而弥补了非对称加密算法处理速度慢的问题,并保证通信内容的机密性。同时,使用非对称加密算法将对称加密算法的密钥进行加密,保证对称加密算法的密钥的安全交换。

这里,介绍一个文件加密与解密的真实案例。需求场景是为了防止平台资源被第三方抓取,因此需要对平台的资源进行加密,并且只能用于平台的客户端使用。为了更好地理解整个通信流程,先来观察一下加密的通信步骤。

资源文件加密流程.png


第一步,客户端 SDK 使用 RSA 加密算法,生成一对公私钥,或者称之为加密密钥与解密密钥。

第二步,客户端 SDK 向服务端请求 AES 密钥,因为密钥保存在服务器的数据库中,由服务端的接口下发密钥。其中,请求参数包括资源 ID 与 RSA 加密算法的公钥。

第三步,服务端使用客户端 SDK请求参数中的资源 ID,生成一个 AES 密钥,并保存到数据库中。

第四步,服务端使用客户端 SDK请求参数中的 RSA 加密算法的公钥,加密AES 密钥。

第五步,服务器发送给客户端 SDK加密后的 AES 密钥。

第六步,客户端 SDK 使用RSA 加密算法的私钥解密,获取到真正的 AES 密钥。

第七步,客户端 SDK 使用真正的 AES 密钥对资源文件进行加密。

理解了资源文件的加密的通信流程,再来观察一下解密的通信步骤。

资源文件解密流程.png


与资源文件的加密的通信流程类似,客户端 SDK 使用 RSA 加密算法,生成一对公私钥,客户端 SDK 向服务端请求 AES 密钥,服务端使用客户端 SDK请求参数中的资源 ID 查询 AES 密钥,服务端使用客户端 SDK请求参数中的 RSA 加密算法的公钥,加密AES 密钥,并且服务器发送给客户端 SDK加密后的 AES 密钥,客户端 SDK 使用RSA 加密算法的私钥解密,获取到真正的 AES 密钥,最后,客户端 SDK 使用真正的 AES 密钥对资源文件进行解密。

HTTPS 降级攻击的场景剖析与解决之道
HTTPS 一定安全么

HTTP 协议没有加密机制,通信内容是明文传输的,没有经过任何安全处理。因此,为了解决窃听、篡改、冒充等风险,采用 HTTPS 协议。HTTPS 可以进行通信内容加密,使第三方无法窃听。HTTPS 可以进行身份认证,一旦通信内容被篡改,通信双方会立刻发现。此外,HTTPS 可以保证通信内容的完整性,防止通信内容冒充或者篡改。那么,使用了 HTTPS 就能确保通信内容的安全传输吗?理论上,是这样的,但是事实上,现实却不是如此,因为设计与实现 SSL/TLS 协议出现了漏洞,导致攻击者同样可以攻击一些旧版本的 SSL/TLS 协议。这其中就包括 SSL 3.0。

什么是 HTTPS 降级攻击

因为浏览器的兼容性问题,当浏览器进行 HTTPS 连接失败的时候,将会尝试使用旧的协议版本,于是加密协议由更加安全的协议,比如 TLS 1.2 降级成 SSL 3.0。因此,在 HTTPS 降级攻击中,攻击者利用旧版本的 SSL/TLS 协议的漏洞,其中包括 SSL 3.0 的漏洞,然后攻击者获取安全连接当中某些可以降级成 SSL 3.0 的加密后的明文的通信内容进行攻击。

注意的是,如果服务器支持 SSL 3.0 协议,同时,攻击者又能作为中间人控制浏览器发起漏洞版本的 HTTPS 请求,此时,虽然攻击者窃听到加密的通信内容,但加密的协议存在漏洞,可以进行降级攻击,解密这些加密的通信内容可以获取有价值的信息,例如用户的隐私数据。

HTTPS 降级攻击的解决之道

目前,解决 HTTPS 降级攻击的唯一方法是禁用 SSL 3.0 协议,并防止 TLS 1.2 协议、 TLS 1.1 协议与 TLS 1.0 协议降级到 SSL 3.0 协议。其中,禁用 SSL 3.0 协议的策略有很多,这里主要介绍下 Nginx 如何防止 TLS 1.2 协议、 TLS 1.1 协议与 TLS 1.0 协议降级到 SSL 3.0 协议以下版本,从而防止 HTTPS 降级攻击。

Nginx 原先的配置,如下所示。此时,如果 Nginx 原先的配置没有额外的配置,那么在 Nginx 中隐性默认是 SSLv3 TLSv1 TLSv1.1 TLSv1.2。
ssl_protocols SSLv3 TLSv1 TLSv1.1 TLSv1.2;

Nginx 禁用 SSL 3.0 协议,并防止 TLS 1.2 协议、 TLS 1.1 协议与 TLS 1.0 协议降级到 SSL 3.0 协议的配置非常简单,只需要屏蔽 SSLv3 参数即可,如下所示。
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;

总结下,HTTPS 能确保通信内容的安全传输吗?答案是不一定,因为设计与实现 SSL/TLS 协议出现了漏洞,导致攻击者同样可以攻击一些旧版本的 SSL/TLS 协议,其中就包括 SSL 3.0,可能会被攻击者进行 HTTPS 的降级攻击。因此,SSL 3.0 协议并不安全,为了防止 HTTPS 的降级攻击,需要禁用 SSL 3.0 协议,确保TLS 1.2 协议、 TLS 1.1 协议与 TLS 1.0 协议降级到 SSL 3.0 协议以下版本。
 
(完)
 


本文作者:梁桂钊
原文链接:http://blog.720ui.com/2017/https_action/
版权归作者所有,转载请注明出处


微服务之构建容错&自动降级的系统 (转载)

xiaobaxi 发表了文章 • 0 个评论 • 878 次浏览 • 2017-06-21 16:39 • 来自相关话题

本文转自https://github.com/JoeCao/JoeCao.github.io  由曹祖鹏编写发布,觉得写的非常不错,就转到社区来了

 微服务是什么

MicroService主要是针对以前的Monolithic(单体)服务来说的,类似优步打车(Uber)这样的应用,如果使用Monolithic来架构,可能是这样的






注:
STRIPE是支付服务
SENDGRID是邮件推送服务
TWILIO是语音、消息的云服务

大部分的项目都是由Monolithic起步的。刚开始最合适不过了,开发简单直接(直接在一个IDE里面就可以调试)、部署方便(搞个war包扔到tomcat里面就能跑)。
但是随着时间的增长,单体应用的缺陷暴露出来了。
首先是代码量大大增加
从几千行到几十万行,接手的程序员已经无法搞清楚里面模块交互逻辑和依赖关系了。大家都在小心翼翼的加代码,避免触碰以前的旧代码。至于单元测试,这种怪兽级别的应用,能不能拆分出来单元啊?
不可避免的超长启动时间
3分钟以内的都算是快的。启动后,程序员抽支烟再回来的不在少数。大量的时间在等待,使得程序员越来越不愿意测试自己的功能,写完了之后直接扔给测试就完事了。测试轮次一次又一次,时间不可避免的浪费。
部署扩展困难
各个模块对服务器的性能要求不同,有的占用CPU高,有的需要大量的IO,有的需要更多的内存。部署在一个服务器内相互影响,扩容的话,无法针对性的扩展。
单个的bug会损坏整个服务
可能一个模块的内存泄露,会使得整个服务都crash down。
对于一些新技术尝试有限制
比如想换个新框架试试,但是要换就全部得换,风险太大。

服务化的改造

解决方案就是服务化,进行下图的Y轴方向的功能拆解。






最终可能的情况类似下图






MicroService不是银弹
服务之间的通讯机制RPC开销大需要考虑分布式情况下的服务失效恢复等等问题数据库分布带来的分布式事务困扰,还有分布式数据结果Join的问题如何管理这么多服务之间的依赖关系,出了问题如何定位?部署服务变得复杂。包括配置、部署、扩容、监控,都是需要专门的工具链支持。

微服务和SOA

虽然微服务很火,但是仅仅把微服务和Monolithic服务做对比,然后搞个大新闻把Monolithic批判一番,我认为是很不公平的。
一般稍具规模企业都在前几年完成了SOA的演化,那么微服务和SOA的区别?SOA在十年前就提出了服务化的概念,那微服务到底是新的理论突破,还是新瓶装旧酒换了一个概念出来忽悠?
总结了微服务应该具备的特点:
小, 且专注于做一件事情独立的进程松耦合、独立部署轻量级的通信机制有边界:
           服务间边界清晰、灵活选择语言和技术(这个是有争议的)
松耦合:
           服务无状态、容错性、故障隔离、自动恢复、弹性伸缩、服务间独立部署,发布、升级、降级互不影响
面向服务架构(SOA) :
           服务化、服务分层

我个人理解SOA和MicroService理论是同源,但是实施方案上面有轻量和重量解决方案的区别,类似当年EJB和Spring之争。
基于ESB、SOAP、WSDL等重型解决方案,演化出来SOA。
利用Dubbo、zookeeper、http/rest、consul、Eureka这些框架的轻量解决方案,也就是现在流行的MicroService。

微服务之后,前端(Web、App)怎么开发?

以电商APP为例,需要购物车、物流服务、库存服务、推荐服务、订单服务、评价服务、商品类目服务....






如果我们让APP分别去调用这些服务会带来什么后果?
后端代码必须改造,因为APP调不了Dubbo协议(二进制的比较复杂),我们得给每个服务都架设一个HTTP代理,也无法利用MicroService的服务发现机制(暴露Zookeeper到公网,非常不安全),必须通过额外的nginx做负载均衡客户端的代码极其冗余,因为业务数据是需要融合的,很多页面需要发送多次请求才能返回给用户一个完成的View,再加上UI 的Callback hell,特别酸爽将内部的逻辑放到了客户端,一旦服务有拆分、融合之类的操作,APP也必须升级

适配移动化的BFF架构

在微服务和APP之间建立一个沟通的桥梁,一个网关(Gateway),我们称之为Backend for Fronter,一个专为前端准备的后端(绕口令中)






BFF职责是路由、聚合、协议转换。
每个请求BFF都可能会调用后面多个微服务的Dubbo接口,将结果聚合后返回给前台。BFF需要给前端更粗颗粒的接口,避免APP的多次调用降低效率。比如/products/{productId}这样的接口会将商品标题、规格、图片、详情页面、评价一股脑的返回给前台。

优缺点

优点:提供了一站式的接口,隐藏内部逻辑,对前端开发友好
缺点:对高可用的架构来说,这个单点的职责较重。

微服务后,中心应该如何开发?

以商品中心为例:一个大的商品中心服务可以被拆为类目、商品、价格、库存四个微服务。这样每个服务的职责专注,接口单一。每个服务可以有自己的存储,甚至可以用不同的存储,比如商品适合Elasticsearch的搜索引擎,库存就适合Redis这样的内存数据库。这样系统的性能提升。
问题又出现了,服务拆开,本来一个接口能返回的数据,必须通过分别调用不同服务的接口来完成。更糟糕的是,不同的服务使用不同的存储引擎,以前一个sql join能解决的事情,变得非常的复杂。
如何解决?
得益于现在技术的提升,我们可以在BFF侧使用zip、join这类的函数式写法替代原来的多层循环来更高效聚合数据。可以使用最新的思路在客户端缓存数据,相当于在app、js侧做了一个数据拷贝的子集(mongodb?),通过diff之类的算法和后端不断同步数据。(类似firebase,野狗的思路)使用类似物化视图的概念,在业务侧生成他需要的视图数据。加快查询速度。






以上方案视情况采用

Backend for Fronter和SOA配合的烦恼

一旦涉及到调用多个后台SOA的接口,SOA接口之间有一定关联关系,需要将结果组合,这组调用的危险性就很大。
以我们常见的商品列表的接口举例,该接口不仅仅是调用了商品中心商品信息接口,还调用了定价服务的用户级别价格接口,然后组合(zip)成为一个商品完整信息返回。
在某些情况下价格服务失效,或者是返回时长波动,那么整个商品列表就一直会阻塞,直到超时也不会返回结果,用户在前台看到的是一个***空白的转圈的页面***。 用户只会认为整个系统都挂了,他可不管你里面发生了什么。
微服务架构成规模后,任何一个我们认为的微不足道的服务的故障,都会导致整个业务的不可用。而在SOA模式下面数千个服务,可能有一定的几率某几个服务是失效的,这种minor的失效导致整个系统不可用的失败,是SOA的一大硬伤。






在后端服务失效这种情况下,我们希望API网关能做到
快速响应用户的请求给用户一些详尽的错误提示,安抚客户如果有可能,降级到某个缓存或者备用服务上快速的跟踪到错误位置防止失效扩散到整个集群如果服务正常了,前端API网关能够自动恢复






Hystrix 是什么?

Hystrix是Netflix开发的一个库,能够实现
调用外部API能够设置超时时间(默认1000ms)限制外部系统的并发调用(默认10个)Circuit Breaker可以在一定错误率后不再调用API(默认50%)Circuit Breaker启动后,每隔五秒可以再次尝试调用API可以监控实时的调用情况,Hystrix甚至提供了一个可视化的监控面板

Hystrix适用场景

Hystrix用在BFF侧,包装外部系统API的调用是最合适的。当然不仅仅是外部系统API调用,即使是内部进程内的调用,也可以用Hystrix的Semaphore模式进行并发限制和监控
Hystrix提供了贴心的Fallback(回落、降级,怎么称呼好呢?)功能,在我们设置的条件满足后,就会转到Fallback方法上,替代原来的接口的返回。
有两个场景,不适合Hystrix的Fallback:
如果接口是write操作
-- 这里的write包括append,update、delete等等操作,这些操作会改变外部系统状态,使用Hystrix是不合适的。因为Hystrix只是在客户端做了限制和监控,Hystrix判定的超时,和外部系统判定的标准并不同步,这就会导致数据不一致。
批量操作或者是离线操作
-- 如果是导出一个订单列表或者统计一个报表之类的操作,就不要Fallback了,让他异步执行吧,没有必要降级。

CircuitBreaker模式

Martin Flower 曾经总结了一个CircuitBreaker的模式,就是在服务故障的情况下,迅速做出反应,不让故障扩散






在图中看到,在可能出现超时的调用中,保护性的调用启动了两个线程,一个线程去实际调用,一个线程监控,如果超过设定的阈值,监控线程会返回一个默认值或者直接抛出异常给客户端,就不等那个真正的远调用线程的返回了
当然这种对超时的控制,一般的RPC都会有(比如Dubbo做了mock的功能)。但是CircuitBreaker的高效之处在于过了一段时间后,可以检测接口的状况,如果接口正常了,自动恢复。
在这里Martin Flower设计了一个“half open”半开的状态







从图上可以看到,在客户端调用次数到了一定的阈值的时候,CircuitBreaker进入了Open状态,在Open状态启动定时器,到了时间会转入Half Open,再次尝试一下接口调用,如果正常了,就恢复。如果还是错误,那就回到Open状态,继续等着吧。

如何集成到我们的SpringBoot的框架

很简单,就是几个Jar包的引入和annotation的使用
请参考例子
http://git.dev.qianmi.com/commons/micro-service-frontend-sample.git @Service
public class PricingApi implements PricingProvider {
@Reference(timeout = 15000, check = false)
PricingProvider pricingProvider;

/**
* 默认在500毫秒内就需要返回结果,否则回落到default的值中
*
* @param itemids
* @return
*/
@Override
@HystrixCommand(fallbackMethod = "defaultPrice", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "500"),
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10")})
public List<ItemPrice> getPrice(String... itemids) {
return pricingProvider.getPrice(itemids);
}

/**如果回落到此方法,使用-1代替价格返回**/
public List<ItemPrice> defaultPrice(String... itemids) {
List<ItemPrice> prices = new ArrayList<>();
for (String itemId : itemids) {
ItemPrice itemPrice = new ItemPrice(itemId, -1.0);
prices.add(itemPrice);
}
return prices;
}
}
只需要通过注解的方式将Dubbo的远程调用包住即可
如果没用Spring boot框架,可以参考这篇文章
https://ahus1.github.io/hystri ... .html

最佳实践
几个重要的参数说明
execution.isolation.thread.timeoutInMilliseconds
这个用于设置超时时间,默认是1000ms
execution.isolation.strategy
这里有两个选择,一个是thread 一个semaphore,thread会启动线程池,默认为10个,主要用于控制外部系统的API调用。semaphore是控制内部的API调用,本机调用(顺便说一下其实Hystrix不仅仅用于RPC场景)





circuitBreaker.requestVolumeThreshold
这个参数决定了,有规定时间内(一般是10s),有多少个错误请求,会触发circuit breaker open。默认20个
circuitBreaker.sleepWindowInMilliseconds
该参数决定了,在circuit break 进入到open状态后,多少时间会再次尝试进入

如何设置线程池的大小







打个比方,经过统计,一个远程接口的返回时间 Median: 40ms 99th: 200ms 99.5th: 300ms  
线程池的计算公式
requests per second at peak when healthy × 99th percentile latency in seconds + some breathing room
99%的请求都会在200ms内返回,一个高峰期并发30个每秒
那么thread pool size = 30 rps * 0.2 seconds = 6 + breathing room(冗余量) =10 threads
99%的请求都会在200ms之内返回,考虑到在失败时,设置一次retry,那么大概就是240ms左右,这样的话设置执行线程的超时时间为300ms。
如果设置得当,一般来说,线程池的Active Thread只会有1-2个,大部分的40ms内的请求会很快的处理掉。
Thread pool设置
@HystrixCommand(commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "500")
},
threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "30"),
@HystrixProperty(name = "maxQueueSize", value = "101"),
@HystrixProperty(name = "keepAliveTimeMinutes", value = "2"),
@HystrixProperty(name = "queueSizeRejectionThreshold", value = "15"),
@HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "12"),
@HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "1440")
})
public User getUserById(String id) {
return userResource.getUserById(id);
}

版权说明

本文采用 CC BY 3.0 CN协议 进行许可。 可自由转载、引用,但需署名作者且注明文章出处 查看全部
本文转自https://github.com/JoeCao/JoeCao.github.io  由曹祖鹏编写发布,觉得写的非常不错,就转到社区来了

 微服务是什么

MicroService主要是针对以前的Monolithic(单体)服务来说的,类似优步打车(Uber)这样的应用,如果使用Monolithic来架构,可能是这样的

1.png


注:
STRIPE是支付服务
SENDGRID是邮件推送服务
TWILIO是语音、消息的云服务

大部分的项目都是由Monolithic起步的。刚开始最合适不过了,开发简单直接(直接在一个IDE里面就可以调试)、部署方便(搞个war包扔到tomcat里面就能跑)。
但是随着时间的增长,单体应用的缺陷暴露出来了。
  • 首先是代码量大大增加

从几千行到几十万行,接手的程序员已经无法搞清楚里面模块交互逻辑和依赖关系了。大家都在小心翼翼的加代码,避免触碰以前的旧代码。至于单元测试,这种怪兽级别的应用,能不能拆分出来单元啊?
  • 不可避免的超长启动时间

3分钟以内的都算是快的。启动后,程序员抽支烟再回来的不在少数。大量的时间在等待,使得程序员越来越不愿意测试自己的功能,写完了之后直接扔给测试就完事了。测试轮次一次又一次,时间不可避免的浪费。
  • 部署扩展困难

各个模块对服务器的性能要求不同,有的占用CPU高,有的需要大量的IO,有的需要更多的内存。部署在一个服务器内相互影响,扩容的话,无法针对性的扩展。
  • 单个的bug会损坏整个服务

可能一个模块的内存泄露,会使得整个服务都crash down。
  • 对于一些新技术尝试有限制

比如想换个新框架试试,但是要换就全部得换,风险太大。

服务化的改造

解决方案就是服务化,进行下图的Y轴方向的功能拆解。

2.png


最终可能的情况类似下图

3.png


MicroService不是银弹
  • 服务之间的通讯机制RPC开销大
  • 需要考虑分布式情况下的服务失效恢复等等问题
  • 数据库分布带来的分布式事务困扰,还有分布式数据结果Join的问题
  • 如何管理这么多服务之间的依赖关系,出了问题如何定位?
  • 部署服务变得复杂。包括配置、部署、扩容、监控,都是需要专门的工具链支持。


微服务和SOA

虽然微服务很火,但是仅仅把微服务和Monolithic服务做对比,然后搞个大新闻把Monolithic批判一番,我认为是很不公平的。
一般稍具规模企业都在前几年完成了SOA的演化,那么微服务和SOA的区别?SOA在十年前就提出了服务化的概念,那微服务到底是新的理论突破,还是新瓶装旧酒换了一个概念出来忽悠?
总结了微服务应该具备的特点:
  • 小, 且专注于做一件事情
  • 独立的进程
  • 松耦合、独立部署
  • 轻量级的通信机制
  • 有边界:

           服务间边界清晰、灵活选择语言和技术(这个是有争议的)
  • 松耦合:

           服务无状态、容错性、故障隔离、自动恢复、弹性伸缩、服务间独立部署,发布、升级、降级互不影响
  • 面向服务架构(SOA) :

           服务化、服务分层

我个人理解SOA和MicroService理论是同源,但是实施方案上面有轻量和重量解决方案的区别,类似当年EJB和Spring之争。
基于ESB、SOAP、WSDL等重型解决方案,演化出来SOA。
利用Dubbo、zookeeper、http/rest、consul、Eureka这些框架的轻量解决方案,也就是现在流行的MicroService。

微服务之后,前端(Web、App)怎么开发?

以电商APP为例,需要购物车、物流服务、库存服务、推荐服务、订单服务、评价服务、商品类目服务....

4.png


如果我们让APP分别去调用这些服务会带来什么后果?
  • 后端代码必须改造,因为APP调不了Dubbo协议(二进制的比较复杂),我们得给每个服务都架设一个HTTP代理,也无法利用MicroService的服务发现机制(暴露Zookeeper到公网,非常不安全),必须通过额外的nginx做负载均衡
  • 客户端的代码极其冗余,因为业务数据是需要融合的,很多页面需要发送多次请求才能返回给用户一个完成的View,再加上UI 的Callback hell,特别酸爽
  • 将内部的逻辑放到了客户端,一旦服务有拆分、融合之类的操作,APP也必须升级


适配移动化的BFF架构

在微服务和APP之间建立一个沟通的桥梁,一个网关(Gateway),我们称之为Backend for Fronter,一个专为前端准备的后端(绕口令中)

5.png


BFF职责是路由、聚合、协议转换。
每个请求BFF都可能会调用后面多个微服务的Dubbo接口,将结果聚合后返回给前台。BFF需要给前端更粗颗粒的接口,避免APP的多次调用降低效率。比如/products/{productId}这样的接口会将商品标题、规格、图片、详情页面、评价一股脑的返回给前台。

优缺点

优点:提供了一站式的接口,隐藏内部逻辑,对前端开发友好
缺点:对高可用的架构来说,这个单点的职责较重。

微服务后,中心应该如何开发?

以商品中心为例:一个大的商品中心服务可以被拆为类目、商品、价格、库存四个微服务。这样每个服务的职责专注,接口单一。每个服务可以有自己的存储,甚至可以用不同的存储,比如商品适合Elasticsearch的搜索引擎,库存就适合Redis这样的内存数据库。这样系统的性能提升。
问题又出现了,服务拆开,本来一个接口能返回的数据,必须通过分别调用不同服务的接口来完成。更糟糕的是,不同的服务使用不同的存储引擎,以前一个sql join能解决的事情,变得非常的复杂。
如何解决?
  • 得益于现在技术的提升,我们可以在BFF侧使用zip、join这类的函数式写法替代原来的多层循环来更高效聚合数据。
  • 可以使用最新的思路在客户端缓存数据,相当于在app、js侧做了一个数据拷贝的子集(mongodb?),通过diff之类的算法和后端不断同步数据。(类似firebase,野狗的思路)
  • 使用类似物化视图的概念,在业务侧生成他需要的视图数据。加快查询速度。


6.png


以上方案视情况采用

Backend for Fronter和SOA配合的烦恼

一旦涉及到调用多个后台SOA的接口,SOA接口之间有一定关联关系,需要将结果组合,这组调用的危险性就很大。
以我们常见的商品列表的接口举例,该接口不仅仅是调用了商品中心商品信息接口,还调用了定价服务的用户级别价格接口,然后组合(zip)成为一个商品完整信息返回。
在某些情况下价格服务失效,或者是返回时长波动,那么整个商品列表就一直会阻塞,直到超时也不会返回结果,用户在前台看到的是一个***空白的转圈的页面***。 用户只会认为整个系统都挂了,他可不管你里面发生了什么。
微服务架构成规模后,任何一个我们认为的微不足道的服务的故障,都会导致整个业务的不可用。而在SOA模式下面数千个服务,可能有一定的几率某几个服务是失效的,这种minor的失效导致整个系统不可用的失败,是SOA的一大硬伤。

7.png


在后端服务失效这种情况下,我们希望API网关能做到
  • 快速响应用户的请求
  • 给用户一些详尽的错误提示,安抚客户
  • 如果有可能,降级到某个缓存或者备用服务上
  • 快速的跟踪到错误位置
  • 防止失效扩散到整个集群
  • 如果服务正常了,前端API网关能够自动恢复


8.png


Hystrix 是什么?

Hystrix是Netflix开发的一个库,能够实现
  • 调用外部API能够设置超时时间(默认1000ms)
  • 限制外部系统的并发调用(默认10个)
  • Circuit Breaker可以在一定错误率后不再调用API(默认50%)
  • Circuit Breaker启动后,每隔五秒可以再次尝试调用API
  • 可以监控实时的调用情况,Hystrix甚至提供了一个可视化的监控面板


Hystrix适用场景

Hystrix用在BFF侧,包装外部系统API的调用是最合适的。当然不仅仅是外部系统API调用,即使是内部进程内的调用,也可以用Hystrix的Semaphore模式进行并发限制和监控
Hystrix提供了贴心的Fallback(回落、降级,怎么称呼好呢?)功能,在我们设置的条件满足后,就会转到Fallback方法上,替代原来的接口的返回。
有两个场景,不适合Hystrix的Fallback:
  • 如果接口是write操作

-- 这里的write包括append,update、delete等等操作,这些操作会改变外部系统状态,使用Hystrix是不合适的。因为Hystrix只是在客户端做了限制和监控,Hystrix判定的超时,和外部系统判定的标准并不同步,这就会导致数据不一致。
  • 批量操作或者是离线操作

-- 如果是导出一个订单列表或者统计一个报表之类的操作,就不要Fallback了,让他异步执行吧,没有必要降级。

CircuitBreaker模式

Martin Flower 曾经总结了一个CircuitBreaker的模式,就是在服务故障的情况下,迅速做出反应,不让故障扩散

9.png


在图中看到,在可能出现超时的调用中,保护性的调用启动了两个线程,一个线程去实际调用,一个线程监控,如果超过设定的阈值,监控线程会返回一个默认值或者直接抛出异常给客户端,就不等那个真正的远调用线程的返回了
当然这种对超时的控制,一般的RPC都会有(比如Dubbo做了mock的功能)。但是CircuitBreaker的高效之处在于过了一段时间后,可以检测接口的状况,如果接口正常了,自动恢复。
在这里Martin Flower设计了一个“half open”半开的状态

10.png



从图上可以看到,在客户端调用次数到了一定的阈值的时候,CircuitBreaker进入了Open状态,在Open状态启动定时器,到了时间会转入Half Open,再次尝试一下接口调用,如果正常了,就恢复。如果还是错误,那就回到Open状态,继续等着吧。

如何集成到我们的SpringBoot的框架

很简单,就是几个Jar包的引入和annotation的使用
请参考例子
http://git.dev.qianmi.com/commons/micro-service-frontend-sample.git 
@Service
public class PricingApi implements PricingProvider {
@Reference(timeout = 15000, check = false)
PricingProvider pricingProvider;

/**
* 默认在500毫秒内就需要返回结果,否则回落到default的值中
*
* @param itemids
* @return
*/
@Override
@HystrixCommand(fallbackMethod = "defaultPrice", commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "500"),
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10")})
public List<ItemPrice> getPrice(String... itemids) {
return pricingProvider.getPrice(itemids);
}

/**如果回落到此方法,使用-1代替价格返回**/
public List<ItemPrice> defaultPrice(String... itemids) {
List<ItemPrice> prices = new ArrayList<>();
for (String itemId : itemids) {
ItemPrice itemPrice = new ItemPrice(itemId, -1.0);
prices.add(itemPrice);
}
return prices;
}
}

只需要通过注解的方式将Dubbo的远程调用包住即可
如果没用Spring boot框架,可以参考这篇文章
https://ahus1.github.io/hystri ... .html

最佳实践
几个重要的参数说明
  • execution.isolation.thread.timeoutInMilliseconds

这个用于设置超时时间,默认是1000ms
  • execution.isolation.strategy

这里有两个选择,一个是thread 一个semaphore,thread会启动线程池,默认为10个,主要用于控制外部系统的API调用。semaphore是控制内部的API调用,本机调用(顺便说一下其实Hystrix不仅仅用于RPC场景)

11.png

  • circuitBreaker.requestVolumeThreshold

这个参数决定了,有规定时间内(一般是10s),有多少个错误请求,会触发circuit breaker open。默认20个
  • circuitBreaker.sleepWindowInMilliseconds

该参数决定了,在circuit break 进入到open状态后,多少时间会再次尝试进入

如何设置线程池的大小


12.png


打个比方,经过统计,一个远程接口的返回时间
 Median: 40ms 99th: 200ms 99.5th: 300ms 
 
  • 线程池的计算公式

requests per second at peak when healthy × 99th percentile latency in seconds + some breathing room
99%的请求都会在200ms内返回,一个高峰期并发30个每秒
那么thread pool size = 30 rps * 0.2 seconds = 6 + breathing room(冗余量) =10 threads
99%的请求都会在200ms之内返回,考虑到在失败时,设置一次retry,那么大概就是240ms左右,这样的话设置执行线程的超时时间为300ms。
如果设置得当,一般来说,线程池的Active Thread只会有1-2个,大部分的40ms内的请求会很快的处理掉。
  • Thread pool设置

@HystrixCommand(commandProperties = {
@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "500")
},
threadPoolProperties = {
@HystrixProperty(name = "coreSize", value = "30"),
@HystrixProperty(name = "maxQueueSize", value = "101"),
@HystrixProperty(name = "keepAliveTimeMinutes", value = "2"),
@HystrixProperty(name = "queueSizeRejectionThreshold", value = "15"),
@HystrixProperty(name = "metrics.rollingStats.numBuckets", value = "12"),
@HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds", value = "1440")
})
public User getUserById(String id) {
return userResource.getUserById(id);
}


版权说明

本文采用 CC BY 3.0 CN协议 进行许可。 可自由转载、引用,但需署名作者且注明文章出处

微服务(Microservices)【翻译】

程序猿DD 发表了文章 • 0 个评论 • 1041 次浏览 • 2017-06-21 13:13 • 来自相关话题

原文是 Martin Flower 于 2014 年 3 月 25 日写的[《Microservices》](http://martinfowler.com/articles/microservices.html)。


微服务

“微服务架构(*Microservice Architecture*)”一词在过去几年里广泛的传播,它用于描述一种设计应用程序的特别方式,作为一套独立可部署的服务。目前,这种架构方式还没有准确的定义,但是在围绕业务能力的组织、自动部署(automated deployment)、端智能(intelligence in the endpoints)、语言和数据的分散控制,却有着某种共同的特征。

“微服务(Microservices)”——只不过在满大街充斥的软件架构中的一新名词而已。尽管我们非常鄙视这样的东西,但是这玩意所描述的软件风格,越来越引起我们的注意。在过去几年里,我们发现越来越多的项目开始使用这种风格,以至于我们身边的同事在构建企业级应用时,把它理所当然的认为这是一种默认开发形式。然而,很不幸,微服务风格是什么,应该怎么开发,关于这样的理论描述却很难找到。

简而言之,微服务架构风格,就像是把一个单独的应用程序开发为一套小服务,每个小服务运行在自己的进程中,并使用轻量级机制通信,通常是 HTTP API。这些服务围绕业务能力来构建,并通过完全自动化部署机制来独立部署。这些服务使用不同的编程语言书写,以及不同数据存储技术,并保持最低限度的集中式管理。

在开始介绍微服务风格(microservice style)前,比较一下整体风格(monolithic style)是很有帮助的:一个完整应用程序(monolithic application)构建成一个单独的单元。企业级应用通常被构建成三个主要部分:客户端用户界面(由运行在客户机器上的浏览器的 HTML 页面、Javascript 组成)、数据库(由许多的表构成一个通用的、相互关联的数据管理系统)、服务端应用。服务端应用处理 HTTP 请求,执行领域逻辑(domain logic),检索并更新数据库中的数据,使用适当的 HTML 视图发送给浏览器。服务端应用是完整的 ,是一个单独的的逻辑执行。任何对系统的改变都涉及到重新构建和部署一个新版本的服务端应用程序。

这样的整体服务(monolithic server)是一种构建系统很自然的方式。虽然你可以利用开发语基础特性把应用程序划分成类、函数、命名空间,但所有你处理请求的逻辑都运行在一个单独的进程中。在某些场景中,开发者可以在的笔计本上开发、测试应用,然后利用部署通道来保证经过正常测试的变更,发布到产品中。你也可以使用横向扩展,通过负载均衡将多个应用部署到多台服务器上。

整体应用程序(Monolithic applications)相当成功,但是越来越多的人感觉到有点不妥,特别是在云中部署时。变更发布周期被绑定了——只是变更应用程序的一小部分,却要求整个重新构建和部署。随着时间的推移,很难再保持一个好的模块化结构,使得一个模块的变更很难不影响到其它模块。扩展就需要整个应用程序的扩展,而不能进行部分扩展。





图 1 整理架构与微服务架构

这导致了微服务架构风格(microservice architectural style)的出现:把应用程序构建为一套服务。事实是,服务可以独立部署和扩展,每个服务提供了一个坚实的模块边界,甚至不同的服务可以用不同的编程语言编写。它们可以被不同的团队管理。

**我们必须说,微服务风格不是什么新东西,它至少可以追溯到 Unix 的设计原则。**但是并没有太多人考虑微服务架构,如果他们用了,那么很多软件都会更好。

微服务风格的特性

微服务风格并没有一个正式的定义,但我们可以尝试描述一下微服务风格所具有的共同特点。并不是所有的微服务风格都要具有所有的特性,但我们期望常见的微服务都应该有这些特性。我们的意图是尝试描述我们工作中或者在其它我们了解的组件中所理解的微服务。特别是,我们不依赖于那些已经明确过的定义。

组件化(Componentization )与服务(Services)

自从我们开始软件行业以来,一直希望由组件构建系统,就像我们在物理世界所看到的一样。在过去的几十年里,我们已经看到了公共库的大量简编取得了相当的进步,这些库是大部分语言平台的一部分。

当我们谈论组件时,可能会陷入一个困境——什么是组件。我们的定义是,组件(component)是一个可独立替换和升级的软件单元。

微服务架构(Microservice architectures)会使用库(libraries),但组件化软件的主要方式是把它拆分成服务。我们把库(libraries)定义为组件,这些组件被链接到程序,并通过内存中函数调用(in-memory function calls)来调用,而服务(services )是进程外组件(out-of-process components),他们利用某个机制通信,比如 WebService 请求,或远程过程调用(remote procedure call)。组件和服务在很多面向对象编程中是不同的概念。

把服务当成组件(而不是组件库)的一个主要原因是,服务可以独立部署。如果你的应用程序是由一个单独进程中的很多库组成,那么对任何一个组件的改变都将导致必须重新部署整个应用程序。但是如果你把应用程序拆分成很多服务,那你只需要重新部署那个改变的服务。当然,这也不是绝对的,有些服务会改变导致协调的服务接口,但是一个好的微服务架构的目标就是通过在服务契约(service contracts)中解耦服务的边界和进化机制来避免这些。

另一个考虑是,把服务当组件将拥有更清晰的组件接口。大多数开发语言都没有一个良好的机制来定义一个[发布的接口](http://martinfowler.com/bliki/ ... e.html)(Published Interface)。发布的接口是指一个类向外公开的成员,比如 Java 中的声明为 Public 的成员,C# 中声明为非 Internal 的成员。通常只有在文档和规范中会说明,这是为了让避免客户端破坏组件的封装性,阻止组件间紧耦合。服务通过使用公开远程调用机制可以很容易避免这些。

像这样使用服务也有不足之处。远程调用比进制内调用更消耗资源,因此远程 API 需要粗粒度(coarser-grained),但这会比较难使用。如果你需要调整组件间的职责分配,当跨越进程边界时,这样做将会很难。

一个可能是,我们看到,服务可以映射到运行时进程(runtime processes)上,但也只是一个可能。服务可以由多个进程组成,它们会同时开发和部署,例如一个应用程序进程和一个只能由这个服务使用的数据库。

围绕业务功能的组织

当寻找把一个大的应用程序拆分成小的部分时,通常管理都会集中在技术层面,UI团队、服务端业务逻辑团队和数据库团队。当使用这种标准对团队进行划分时,甚至小小的更变都将导致跨团队项目协作,从而消耗时间和预算审批。一个高效的团队会针对这种情况进行改善,两权相害取其轻。业务逻辑无处不在。实践中,这就是 Conway's Law 的一个例子。设计一个系统的任何组织(广义上)都会产生这样一种设计,其结构是组织交流结构的复制。
——Melvyn Conway, 1967
Melvyn Conway 的意识是,像下图所展示的,设计一个系统时,将人员划分为 UI 团队,中间件团队,DBA 团队,那么相应地,软件系统也就会自然地被划分为 UI 界面,中间件系统,数据库。





图 2  实践中的 Conway's Law

微服务(microservice )的划分方法不同,它倾向围绕业务功能的组织来分割服务。这些服务实现商业领域的软件,包括用户界面,持久化存储,任何的外部协作。因此,团队是跨职能的(cross-functional),包含开发过程所要求的所有技能:用户体验(user-experience)、数据库(database)和项目管理(project management)。





图 3 通过团队边界强调服务边界

[www.comparethemarket.com](http://www.comparethemarket.com/)就是采用这种组织形式。跨职能的团队同时负责构建和运营每个产品,每个产品被分割成许多单个的服务,这些服务通过消息总线(Message Bus)通信。

大型的整体应用程序(monolithic applications)也可以按照业务功能进行模块化(modularized),尽管这样情况不常见。当然,我们可以敦促一个构建整体应用程序(monolithic application )的大型团队,按业务线来分割自己。我们已经看到的主要问题是,这种组件形式会导致很多的依赖。如果整体应用程序(monolithic applications)跨越很多模块边界(modular boundaries ),那么对于团队的每个成员短期内修复它们是很困难的。此外,我们发现,模块化需要大量的强制规范。服务组件所要求的必需的更明确的分离使得保持团队边界清晰更加容易。

产品不是项目

大部分的软件开发者都使用这样的项目模式:至力于提供一些被认为是完整的软件。交付一个他们认为完成的软件。软件移交给运维组织,然后,解散构建软件的团队。

微服务(Microservice )的支持者认为这种做法是不可取的,并提议团队应该负责产品的整个生命周期。Amazon 理念是“[你构建,你运维(you build, you run it)](https://queue.acm.org/detail.cfm?id=1142065)”,要求开发团队对软件产品的整个生命周期负责。这要求开发者每天都关注他们的软件运行如何,增加更用户的联系,同时承担一些售后支持。

产品的理念,跟业务能力联系起来。不是着眼于完成一套功能的软件,而是有一个持续的关系,是如何能够帮助软件及其用户提升业务能力。

为什么相同的方法不能用在整体应用程序(monolithic applications),但更小的服务粒度能够使创建服务的开发者与使用者之间的个人联系更容易。

强化终端及弱化通道

当构建不同的进程间通信机制的时候,我们发现有许多的产品和方法能够把更加有效方法强加入的通信机制中。比如企业服务总线(ESB),这样的产品提供更有效的方式改进通信过程中的路由、编码、传输、以及业务处理规则。

微服务倾向于做如下的选择:强化终端及弱化通道。微服务的应用致力松耦合和高内聚:采用单独的业务逻辑,表现的更像经典Unix意义上的过滤器一样,接受请求、处理业务逻辑、返回响应。它们更喜欢简单的REST风格,而不是复杂的协议,如WS或者BPEL或者集中式框架。

有两种协议最经常被使用到:包含资源API的HTTP的请求-响应和轻量级消息通信协议。最为重要的建议为:善于利用网络,而不是限制(Be of the web, not behind the web)。
——Ian Robinson
微服务团队采用这样的原则和规范:基于互联网(广义上,包含Unix系统)构建系统。这样经常使用的资源几乎不用什么的代价就可以被开发者或者运行商缓存。

第二种做法是通过轻量级消息总线来发布消息。这种的通信协议非常的单一(单一到只负责消息路由),像RabbitMQ或者ZeroMQ这样的简单的实现甚至像可靠的异步机制都没提供,以至于需要依赖产生或者消费消息的终端或者服务来处理这类问题。

在整体工风格中,组件在进程内执行,进程间的消息通信通常通过调用方法或者回调函数。从整体式风格到微服务框架最大的问题在于通信方式的变更。从内存内部原始的调用变成远程调用,产生的大量的不可靠通信。因此,你需要把粗粒度的方法成更加细粒度的通信。

分散治理

集中治理的一种好处是在单一平台上进行标准化。经验表明这种趋势的好处在缩小,因为并不是所有的问题都相同,而且解决方案并不是万能的。我们更加倾向于采用适当的工具解决适当的问题,整体式的应用在一定程度上比多语言环境更有优势,但也适合所有的情况。

把整体式框架中的组件,拆分成不同的服务,我们在构建它们时有更多的选择。你想用Node.js去开发报表页面吗?做吧。用C++来构建时时性要求高的组件?很好。你想以在不同类型的数据库中切换,来提高组件的读取性能?我们现在有技术手段来实现它了。

当然,你是可以做更多的选择,但也不意味的你就可以这样做,因为你的系统使用这种方式进行侵害意味着你已经有的决定。

采用微服务的团队更喜欢不同的标准。他们不会把这些标准写在纸上,而是喜欢这样的思想:开发有用的工具来解决开发者遇到的相似的问题。这些工具通常从实现中成长起来,并进行的广泛范围内分享,当然,它们有时,并不一定,会采用开源模式。现在开源的做法也变得越来越普遍,git或者github成为了它们事实上的版本控制系统。

Netfix就是这样的一个组织,它是非常好的一个例子。分享有用的、尤其是经过实践的代码库激励着其它的开发着也使用相似的方式来解决相似的问题,当然,也保留着根据需要使用不同的方法的权力。共享库更关注于数据存储、进程内通信以及我们接下来做讨论到的自动化等这些问题上。

微服务社区中,开销问题特别引人注意。这并不是说,社区不认为服务交互的价值。相反,正是因为发现到它的价值。这使得他们在寻找各种方法来解决它们。如[Tolearant Reader](http://martinfowler.com/bliki/TolerantReader.html)和[Consumer-Driven Contracts](http://martinfowler.com/articl ... s.html)这样的设计模式就经常被微服务使用。这些模式解决了独立服务在交互过程中的消耗问题。使用Consumer-Driven Contracts增加了你的信心,并实现了快速的反馈机制。事实上,我们知道澳大利亚的一个团队致力使用Consumer-Drvien Contracts开发新的服务。他们使用简单的工程,帮助他们定义服务的接口。使得在新服务的代码开始编写之前,这些接口就成为自动化构建的一个部分。构建出来的服务,只需要指出这些接口适用的范围,一个优雅的方法避免了新软件中的'YAGNI '困境。这些技术和工具在使用过程中完善,通过减少服务间的耦合,限制了集中式管理的需求。

也许分散治理普及于亚马逊“编译它,运维它”的理念。团队为他们开发的软件负全部责任,也包含7*24小时的运行。全责任的方式并不常见,但是我们确实发现越来越多的公司在他们的团队中所推广。Netfix是另外一个接受这种理念的组件。每天凌晨3点被闹钟吵醒,因为你非常的关注写的代码质量。这在传统的集中式治理中这是一样多么不思议的事情呀。

分散数据管理

对数据的分散管理有多种不同的表现形式。最为抽象层次,它意味着不同系统中的通用概念是不同的。这带来的觉问题是大型的跨系统整合时,用户使用不同的售后支持将得到不同的促销信息。这种情况叫做并没有给用户显示所有的促销手段。不同的语法确实存在相同的词义或者(更差)相同的词义。

应用之间这个问题很普遍,但应用内部这个问题也存在,特别是当应用拆分成不同的组件时。对待这个问题非常有用的方式为[Bounded Context](http://martinfowler.com/bliki/BoundedContext.html)的领域驱动设计。DDD把复杂的领域拆分成不同上下文边界以及它们之间的关系。这样的过程对于整体架构和微服务框架都很有用,但是服务间存在着明显的关系,帮助我们对上下文边界进行区分,同时也像我们在业务功能中谈到的,强行拆分。

当对概念模式下决心进行分散管理时,微服务也决定着分散数据管理。当整体式的应用使用单一逻辑数据库对数据持久化时,企业通常选择在应用的范围内使用一个数据库,这些决定也受厂商的商业权限模式驱动。微服务让每个服务管理自己的数据库:无论是相同数据库的不同实例,或者是不同的数据库系统。这种方法叫[Polyglot Persistence](http://martinfowler.com/bliki/ ... e.html)。你可以把这种方法用在整体架构中,但是它更常见于微服务架构中。





图 4 Polyglot Persistence

微服务音分散数据现任意味着管理数据更新。处理数据更新的常用方法是使用事务来保证不同的资源修改数据库的一致性。这种方法通常在整体架构中使用。

使用事务是因为它能够帮助处理一至性问题,但对时间的消耗是严重的,这给跨服务操作带来难题。分布式事务非常难以实施,因此微服务架构[强调服务间事务的协调](http://www.eaipatterns.com/ram ... s.html),并清楚的认识一致性只能是最终一致性以及通过补偿运算处理问题。

选择处理不一致问题对于开发团队来说是新的挑战,但是也是一个常见的业务实践模式。通常业务上允许一定的不一致以满足快速响应的需求,但同时也采用一些恢复的进程来处理这种错误。当业务上处理强一致性消耗比处理错误的消耗少时,这种付出是值的的。

基础设施自动化

基础设施自动化技术在过去几年中得到了长足的发展:云计算,特别是AWS的发展,减少了构建、发布、运维微服务的复杂性。

许多使用微服务架构的产品或者系统,它们的团队拥有丰富的[持集部署](http://martinfowler.com/bliki/ ... y.html)以及它的前任[持续集成](http://martinfowler.com/articl ... n.html)的经验。团队使用这种方式构建软件致使更广泛的依赖基础设施自动化技术。下图说明这种构建的流程:





图 5 基本的构建流程

尽管这不是介绍自动部署的文章,但我们也打算介绍一下它的主要特征。我们希望我们的软件应该这样方便的工作,因此我们需要更多的自动化测试。流程中工作的软件改进意味着我们能自动的部署到各种新的环境中。

整体风格的应用相当开心的在各种环境中构建、测试、发布。事实证明,一旦你打算投资一条整体架构应用自动化的的生产线,那么你会发现发布更多的应用似乎非不那么的可怕。记住,CD(持续部署)的一个目标在于让发布变得无趣,因此无论是一个还是三个应用,它都一样的无趣。

另一个方面,我们发现使用微服务的团队更加依赖于基础设施的自动化。相比之下,在整体架构也微服务架构中,尽管发布的场景不同,但发布工作的无趣并没有多大的区别。





图 6 模块化部署的区别

容错性设计

使用服务作为组件的一个结果在于应用需要有能容忍服务的故障的设计。任务服务可能因为供应商的不可靠而故障,客户端需要尽可能的优化这种场景的响应。跟整体构架相比,这是一个缺点,因为它带来的额外的复杂性。这将让微服务团队时刻的想到服务故障的情况下用户的体验。Netflix 的[Simian Army](https://github.com/Netflix/SimianArmy)可以为每个应用的服务及数据中心提供日常故障检测和恢复。

这种产品中的自动化测试可以让大部分的运维团队正常的上下班。这并不意味着整体构架的应用没有这么精巧的监控配置,只是在我们的经验中它并不常见。

由于服务可以随时故障,快速故障检测,乃至,自动恢复变更非常重要。微服务应用把实时的监控放在应用的各个阶段中,检测构架元素(每秒数据库的接收的请求数)和业务相关的指标(把分钟接收的定单数)。监控系统可以提供一种早期故障告警系统,让开发团队跟进并调查。

对于微服务框架来说,这相当重要,因为微服务相互的通信可能导致紧急意外行为。许多专家车称赞这种紧急事件的价值,但事实是这种紧急行为有时是灾难。监控是至关重要的,它能快速发现这种紧急不良行为,让我们迅速修复它。

整体架构,跟微服务一样,在构建时是通明的,实情上,它们就是这样子的。它们不同之处在于,你需要清楚的认识到不同进程间运行的服务是不相关的。库对于同一进程是透明的,也因此不那么重要了。

微服务团队期望清楚的监控和记录每个服务的配置,比如使用仪表盘显示上/下线状态、各种运维和业务相关的指标。对断路器(circuit breaker)状态、目前的吞吐量和时延细节,我们也会经常遇到。

设计改进

微服务实践者,通常有不断改进设计的背景,他们把服务分解成进一步的工具。这些工具可以让应用开发者在不改变速度情况下,控制都他们的应用的需求变更。变更控制不意味首减少变更,而是使用适当的方式和工具,让它更加频繁,至少,很好让它变得可控。

不论如何,当你试图软件系统拆分成组件时,你将面临着如何拆分的问题。那么我们的决定拆分我们应用的原则是什么呢?首要的因素,组件可以被独立替换和更新的,这意味着我们寻找的关键在于,我们要想象着重写一个组件而不影响它们之前的协作关系。事实上,许多的微服务小组给它进一步的预期:服务应该能够报废的,而不是要长久的发展的。

Guardian网站就是这方面的一个优秀的例子,它初期被设计和构建成一个整体架构,但它已经向微服务的发展了。整体构架仍然是它网站的核心,但是他们使用微服务来增加那些使用整体架构API的新特性。这种方法增加这些临时的特性非常方便,比如运动新闻的特稿。这样站点的一个部分可以使用快速的开发语言迅速整合起来,当它过时后可以一次性移除。我们发现一家金融机构用相似的方法增加新的市场营销活动,数周或者几个月后把它撤销。

可代替是模块化开发中的一个特例,它是用模块来应对需要变更的。你希望让变更是相同模块,相同周期中进行变化而已。系统的某些很小做变更部分,也应该放在不同的服务中,这样它们更容易让它们消亡。如果你发现两个服务一直重复的变更时,这就是一个要合并它们的信号了。

把组件改成服务,增加了细化发布计划的一个机会。整体构架的任务变更需要整个应用的完整的构建和发布。然而,使用微服务,你只需要发布你要修改的服务就可以了。这将简化和加速你的发布周期。缺点是你需要为一个变更服务发布可能中断用户的体验而担心。传统的集成方法是使用版本来处理这些问题,但是微服务[版本仅是最后的通告手段](http://martinfowler.com/articl ... ioning)。我们需要在设计服务时尽可能的容忍供应商的变更,以避免提供多个版本。

微服务是未来吗?

我们写这篇文章的主要目的在于解释微服务的主要思想和原则。但是发时间做这事的时候,我们清醒的认识到微服务构架风格是一个非常重要的想法:一个值得企业应用中认真考虑的东西。我们最近使用这种风格构建了几个系统,认识那些也使用和喜欢这种方法的爱好者。

我们认识的使用这种方式的先行者,包含亚马逊、Netflix、The Guardian、The UK Government Digital Service、realestate.com.au、Forward和comparethemarket.com。2013看的巡回会议充满了向正在想成为微服务一分子的公司,包含Travis CI。此外,大量的组件正在从事我们认为是微服务的事,只是没有使用微服务的名字而已。(通常,它们被打上SOA的标签,尽管,我们认为SOA有许多不同的地方。)

尽管有这些积极的经验,然后,我们也不急于确认微服务是未来软件架构方向。至今为止,我们的经验与整体风格的应该中相比出来的是有优势的,但是我们意识知这样的事实,我们并没有足够的时间来证明我们的论证。

你所使用的架构通常是你开发出来后,使用的几年的实际成果。我们看到这些工程是在一个优秀的团队,带着对模块化的强烈追求,使用在过去几年中已经衰退的整体架构构建出来的。许多人相信,这种衰退不太可能与微服务有关,因为服务边界是清晰的并且很难再完善的。然而,当我们还没看到足够多的系统运行足够长时间时,我们不能肯定微服务构架是成熟的。

当然,还有原因就是,有人期望微服务构架不够成熟。在组件化方面的任何努力,其成功都依赖于软件如何拆分成适合的组件。指出组件化的准确边界应该在那,这是非常困难的。改良设计要承认边界的权益困境和因此带来的易于重构的重要性。但是当你的组件是被远程通信的服务时,重构比进程内的库又要困难的多。服务边界上的代码迁移是困难的,任务接口的变更需要参与者的共同协作,向后兼容的层次需要被增加,测试也变更更加复杂。

另一个问题在于,如果组件并没有清晰的划分,你的工作的复杂性将从组件内部转向组件间的关系。做这事不仅要围绕着复杂,它也要面对着不清晰和更难控制的地方。很容易想到,当你在一个小的、简单的组件内找东西,总比在没有关系的混乱的服务间要容易。

最后,团队技能也是重要的因素。新的技术倾向于被掌握更多的技能的团队使用。但是掌握多技能的团队中使用的技巧在较少技能的团队中并不是必需的。我们发现大量的少技能的团队构建混乱的整合构架,但是它要发时间去证明使用微服务在这种情况下会发生什么。一个糟糕的团队通常开发糟糕的系统:很难说,微服务在这种情况下是否能帮助它们,还是破坏它们。

  一个理性的争议在于,我们听说,你不应该从微服务构架开始做。最好从整体构架开发,做模块化开发,然后当整体构架出现问题是再把模块化拆分成服务。(尽管这种建议不是好主意,因为一个好的进程内接口并不是一个好的服务接口。)

因此我们持这种谨慎的乐观。到目前为止,我们还没有足够认识,关于微构架能否被大范围的推广。我们不能肯定的说,我们要终结什么,但是软件开发的挑战在于你只能在不完整的信息中决定你目前要处理的问题。

其它

微服务系统多大?

尽管“微服务”一词在架构风格中越来越流行,它的名字很不辛让人关注它的服务大小,以及对“微”这个组成的争议。在我们与微服务实践者的谈话中,我们发现了服务的大小范围。被报道的最大团队遵循亚马逊Tow Pizaa团队理念(比如,一个团队吃两个比萨就可以了。),这意味着不超过20号(一打)人。我们发现最小配置是半打的团队支撑起一打的服务。

这也引发这样的考虑:规模为一个服务一打人到一个服务一个人的团队打上微服务的标签。此刻我们认为,它们是一样的,但是随着对这种风格的深入研究,也存在我们改变我们的想法的可能。

微服务与SOA

当前我们谈到微服务时,通常会问,这是不是我们20年前讨论的面向服务架构(SOA)。这是一个很好的观点,因为微服务风格也SOA所提倡的一些优势非常相似。尽管如此,问题在于SOA意味的[太多不同的东西](http://martinfowler.com/bliki/ ... y.html)了,因此通常时候我们谈的所谓“SOA”时,它与我们谈论的风格不一至,因为它通常是指在整体风格应用中的ESB。

此外,我们发现面向服务的风格是这么的拙劣:从试图使用ESB隐藏复杂性, 到使用多年才认识到发费数百美元却没产生任务价值这样的失败,到集中治理模式抑制变更。而且这些问题往往很难发现。

可以肯定的时,微服务社区中使用的许多的技术都开发者是从大型机构的整合服务经验中发展来的。[Tolerant Reader](http://martinfowler.com/bliki/TolerantReader.html)模式就是这样的一个例子。由于互联网的发展,利用简单的协议这种方法,让它从这些经验传达的出来。这是从已经很复杂的集中式标准中的一种反模式,坦白的说,真让人惊叹。(无论何时,当你需要用一个服务来管理你的所有的服务,你就知道这很麻烦。)

SOA的这种常见行为让微服务的提倡者拒绝打上SOA的标签,尽管有人认为微服务是从SOA中发展而来的,或许面向服务是对的。无论如何,事实上SOA表达这么多的含义,它给一个团队清醒的认识到这种构架风格就已经值的了。

多语言,多选择

JVM做为一个平台,它的增长就是一个平台中运行多语言的最大的例子。过去二十年中,它通常做为更高层次语言的壳,以达到更高层次的抽象。比如,研究它的内部结构,、使用低级的语言写更高效的代码。尽管如此,许多整体风格并不需要这种层次的性能优化或者在语法及高层次上的抽象,这很常见(让我们很失望)。此外整体构架通常意味着使用单一的语言,这也限制着使用技术的数量。

实践标准和强制标准

它有点尴尬,微服务团队倾向于避免这种通常由企业架构队伍定制的僵硬的强制标准,但是它们却非常乐于甚至推广这些开放的标准,如HTTP、ATOM、其它微规范。

关键的不同在这些标准是怎么开发出来的,以及它们是怎么被推广的。标准被一些组件管理,如IETF认证标准,仅当它们在互联网上有几个在用的实现,通常源自于开源工程的成功应用。

这些标准单独分离出来,与那种在企业中通常有没有什么编码经验的或者没有什么影响力的厂商标准进行区别。

让做对事更容易

一方面,我们发现在持续发布、部署越来越多的使用自动化,是很多有用的工具开发出来帮助开发者和运营商的努力结果。为打包、代码管理、支撑服务的工具,或者增加标准监控的记录的工具,现在都非常常见了。网络中最好的,可能就是[Netflix's的开源工具](http://netflix.github.io/),但是包含[Dropwizard](http://dropwizard.codahale.com/)在内的其它工具也被广泛的使用着。

断路器(circuit breaker)和产品中现有的代码

[断路器](http://martinfowler.com/bliki/CircuitBreaker.html)(circuit breaker)出现在《[Realease It!](http://www.amazon.com/gp/produ ... 32NXZO)》一书中,与Bulkhead和Timeout这样的模式放在一起。实施起来,这些模式用于构建通信应用时相当的重要。[Netflix的博客](http://techblog.netflix.com/20 ... e.html)在解释它们的应用时,做了大量的工作。

同步是有害的

任务时候,你在服务间的调用使用同步的方法,都会遇到宕机时间的乘积效应。简单的说,你的系统宕机时间是你系统的单独组件的宕机时间的乘积。你面临的选择使用异步或者管理宕机时间。在[www.guardian.co.uk](http://www.guardian.co.uk/)中,它们在新平台中使用一种简单的规则来实现它:在Netflix中每次用户请求的同步调用,他们重新设计的平台API都会把它构建成异步的API来执行。

本文作者:船长&CAP
原文链接:http://www.cnblogs.com/liuning8023/p/4493156.html
版权归作者所有,转载请注明出处 查看全部


原文是 Martin Flower 于 2014 年 3 月 25 日写的[《Microservices》](http://martinfowler.com/articles/microservices.html)。



微服务

“微服务架构(*Microservice Architecture*)”一词在过去几年里广泛的传播,它用于描述一种设计应用程序的特别方式,作为一套独立可部署的服务。目前,这种架构方式还没有准确的定义,但是在围绕业务能力的组织、自动部署(automated deployment)、端智能(intelligence in the endpoints)、语言和数据的分散控制,却有着某种共同的特征。

“微服务(Microservices)”——只不过在满大街充斥的软件架构中的一新名词而已。尽管我们非常鄙视这样的东西,但是这玩意所描述的软件风格,越来越引起我们的注意。在过去几年里,我们发现越来越多的项目开始使用这种风格,以至于我们身边的同事在构建企业级应用时,把它理所当然的认为这是一种默认开发形式。然而,很不幸,微服务风格是什么,应该怎么开发,关于这样的理论描述却很难找到。

简而言之,微服务架构风格,就像是把一个单独的应用程序开发为一套小服务,每个小服务运行在自己的进程中,并使用轻量级机制通信,通常是 HTTP API。这些服务围绕业务能力来构建,并通过完全自动化部署机制来独立部署。这些服务使用不同的编程语言书写,以及不同数据存储技术,并保持最低限度的集中式管理。

在开始介绍微服务风格(microservice style)前,比较一下整体风格(monolithic style)是很有帮助的:一个完整应用程序(monolithic application)构建成一个单独的单元。企业级应用通常被构建成三个主要部分:客户端用户界面(由运行在客户机器上的浏览器的 HTML 页面、Javascript 组成)、数据库(由许多的表构成一个通用的、相互关联的数据管理系统)、服务端应用。服务端应用处理 HTTP 请求,执行领域逻辑(domain logic),检索并更新数据库中的数据,使用适当的 HTML 视图发送给浏览器。服务端应用是完整的 ,是一个单独的的逻辑执行。任何对系统的改变都涉及到重新构建和部署一个新版本的服务端应用程序。

这样的整体服务(monolithic server)是一种构建系统很自然的方式。虽然你可以利用开发语基础特性把应用程序划分成类、函数、命名空间,但所有你处理请求的逻辑都运行在一个单独的进程中。在某些场景中,开发者可以在的笔计本上开发、测试应用,然后利用部署通道来保证经过正常测试的变更,发布到产品中。你也可以使用横向扩展,通过负载均衡将多个应用部署到多台服务器上。

整体应用程序(Monolithic applications)相当成功,但是越来越多的人感觉到有点不妥,特别是在云中部署时。变更发布周期被绑定了——只是变更应用程序的一小部分,却要求整个重新构建和部署。随着时间的推移,很难再保持一个好的模块化结构,使得一个模块的变更很难不影响到其它模块。扩展就需要整个应用程序的扩展,而不能进行部分扩展。

microservices-translate-1.png

图 1 整理架构与微服务架构

这导致了微服务架构风格(microservice architectural style)的出现:把应用程序构建为一套服务。事实是,服务可以独立部署和扩展,每个服务提供了一个坚实的模块边界,甚至不同的服务可以用不同的编程语言编写。它们可以被不同的团队管理。

**我们必须说,微服务风格不是什么新东西,它至少可以追溯到 Unix 的设计原则。**但是并没有太多人考虑微服务架构,如果他们用了,那么很多软件都会更好。

微服务风格的特性

微服务风格并没有一个正式的定义,但我们可以尝试描述一下微服务风格所具有的共同特点。并不是所有的微服务风格都要具有所有的特性,但我们期望常见的微服务都应该有这些特性。我们的意图是尝试描述我们工作中或者在其它我们了解的组件中所理解的微服务。特别是,我们不依赖于那些已经明确过的定义。

组件化(Componentization )与服务(Services)

自从我们开始软件行业以来,一直希望由组件构建系统,就像我们在物理世界所看到的一样。在过去的几十年里,我们已经看到了公共库的大量简编取得了相当的进步,这些库是大部分语言平台的一部分。

当我们谈论组件时,可能会陷入一个困境——什么是组件。我们的定义是,组件(component)是一个可独立替换和升级的软件单元。

微服务架构(Microservice architectures)会使用库(libraries),但组件化软件的主要方式是把它拆分成服务。我们把库(libraries)定义为组件,这些组件被链接到程序,并通过内存中函数调用(in-memory function calls)来调用,而服务(services )是进程外组件(out-of-process components),他们利用某个机制通信,比如 WebService 请求,或远程过程调用(remote procedure call)。组件和服务在很多面向对象编程中是不同的概念。

把服务当成组件(而不是组件库)的一个主要原因是,服务可以独立部署。如果你的应用程序是由一个单独进程中的很多库组成,那么对任何一个组件的改变都将导致必须重新部署整个应用程序。但是如果你把应用程序拆分成很多服务,那你只需要重新部署那个改变的服务。当然,这也不是绝对的,有些服务会改变导致协调的服务接口,但是一个好的微服务架构的目标就是通过在服务契约(service contracts)中解耦服务的边界和进化机制来避免这些。

另一个考虑是,把服务当组件将拥有更清晰的组件接口。大多数开发语言都没有一个良好的机制来定义一个[发布的接口](http://martinfowler.com/bliki/ ... e.html)(Published Interface)。发布的接口是指一个类向外公开的成员,比如 Java 中的声明为 Public 的成员,C# 中声明为非 Internal 的成员。通常只有在文档和规范中会说明,这是为了让避免客户端破坏组件的封装性,阻止组件间紧耦合。服务通过使用公开远程调用机制可以很容易避免这些。

像这样使用服务也有不足之处。远程调用比进制内调用更消耗资源,因此远程 API 需要粗粒度(coarser-grained),但这会比较难使用。如果你需要调整组件间的职责分配,当跨越进程边界时,这样做将会很难。

一个可能是,我们看到,服务可以映射到运行时进程(runtime processes)上,但也只是一个可能。服务可以由多个进程组成,它们会同时开发和部署,例如一个应用程序进程和一个只能由这个服务使用的数据库。

围绕业务功能的组织

当寻找把一个大的应用程序拆分成小的部分时,通常管理都会集中在技术层面,UI团队、服务端业务逻辑团队和数据库团队。当使用这种标准对团队进行划分时,甚至小小的更变都将导致跨团队项目协作,从而消耗时间和预算审批。一个高效的团队会针对这种情况进行改善,两权相害取其轻。业务逻辑无处不在。实践中,这就是 Conway's Law 的一个例子。
设计一个系统的任何组织(广义上)都会产生这样一种设计,其结构是组织交流结构的复制。
——Melvyn Conway, 1967

Melvyn Conway 的意识是,像下图所展示的,设计一个系统时,将人员划分为 UI 团队,中间件团队,DBA 团队,那么相应地,软件系统也就会自然地被划分为 UI 界面,中间件系统,数据库。

microservices-translate-2.png

图 2  实践中的 Conway's Law

微服务(microservice )的划分方法不同,它倾向围绕业务功能的组织来分割服务。这些服务实现商业领域的软件,包括用户界面,持久化存储,任何的外部协作。因此,团队是跨职能的(cross-functional),包含开发过程所要求的所有技能:用户体验(user-experience)、数据库(database)和项目管理(project management)。

microservices-translate-3.png

图 3 通过团队边界强调服务边界

[www.comparethemarket.com](http://www.comparethemarket.com/)就是采用这种组织形式。跨职能的团队同时负责构建和运营每个产品,每个产品被分割成许多单个的服务,这些服务通过消息总线(Message Bus)通信。

大型的整体应用程序(monolithic applications)也可以按照业务功能进行模块化(modularized),尽管这样情况不常见。当然,我们可以敦促一个构建整体应用程序(monolithic application )的大型团队,按业务线来分割自己。我们已经看到的主要问题是,这种组件形式会导致很多的依赖。如果整体应用程序(monolithic applications)跨越很多模块边界(modular boundaries ),那么对于团队的每个成员短期内修复它们是很困难的。此外,我们发现,模块化需要大量的强制规范。服务组件所要求的必需的更明确的分离使得保持团队边界清晰更加容易。

产品不是项目

大部分的软件开发者都使用这样的项目模式:至力于提供一些被认为是完整的软件。交付一个他们认为完成的软件。软件移交给运维组织,然后,解散构建软件的团队。

微服务(Microservice )的支持者认为这种做法是不可取的,并提议团队应该负责产品的整个生命周期。Amazon 理念是“[你构建,你运维(you build, you run it)](https://queue.acm.org/detail.cfm?id=1142065)”,要求开发团队对软件产品的整个生命周期负责。这要求开发者每天都关注他们的软件运行如何,增加更用户的联系,同时承担一些售后支持。

产品的理念,跟业务能力联系起来。不是着眼于完成一套功能的软件,而是有一个持续的关系,是如何能够帮助软件及其用户提升业务能力。

为什么相同的方法不能用在整体应用程序(monolithic applications),但更小的服务粒度能够使创建服务的开发者与使用者之间的个人联系更容易。

强化终端及弱化通道

当构建不同的进程间通信机制的时候,我们发现有许多的产品和方法能够把更加有效方法强加入的通信机制中。比如企业服务总线(ESB),这样的产品提供更有效的方式改进通信过程中的路由、编码、传输、以及业务处理规则。

微服务倾向于做如下的选择:强化终端及弱化通道。微服务的应用致力松耦合和高内聚:采用单独的业务逻辑,表现的更像经典Unix意义上的过滤器一样,接受请求、处理业务逻辑、返回响应。它们更喜欢简单的REST风格,而不是复杂的协议,如WS或者BPEL或者集中式框架。

有两种协议最经常被使用到:包含资源API的HTTP的请求-响应和轻量级消息通信协议。最为重要的建议为:
善于利用网络,而不是限制(Be of the web, not behind the web)。
——Ian Robinson

微服务团队采用这样的原则和规范:基于互联网(广义上,包含Unix系统)构建系统。这样经常使用的资源几乎不用什么的代价就可以被开发者或者运行商缓存。

第二种做法是通过轻量级消息总线来发布消息。这种的通信协议非常的单一(单一到只负责消息路由),像RabbitMQ或者ZeroMQ这样的简单的实现甚至像可靠的异步机制都没提供,以至于需要依赖产生或者消费消息的终端或者服务来处理这类问题。

在整体工风格中,组件在进程内执行,进程间的消息通信通常通过调用方法或者回调函数。从整体式风格到微服务框架最大的问题在于通信方式的变更。从内存内部原始的调用变成远程调用,产生的大量的不可靠通信。因此,你需要把粗粒度的方法成更加细粒度的通信。

分散治理

集中治理的一种好处是在单一平台上进行标准化。经验表明这种趋势的好处在缩小,因为并不是所有的问题都相同,而且解决方案并不是万能的。我们更加倾向于采用适当的工具解决适当的问题,整体式的应用在一定程度上比多语言环境更有优势,但也适合所有的情况。

把整体式框架中的组件,拆分成不同的服务,我们在构建它们时有更多的选择。你想用Node.js去开发报表页面吗?做吧。用C++来构建时时性要求高的组件?很好。你想以在不同类型的数据库中切换,来提高组件的读取性能?我们现在有技术手段来实现它了。

当然,你是可以做更多的选择,但也不意味的你就可以这样做,因为你的系统使用这种方式进行侵害意味着你已经有的决定。

采用微服务的团队更喜欢不同的标准。他们不会把这些标准写在纸上,而是喜欢这样的思想:开发有用的工具来解决开发者遇到的相似的问题。这些工具通常从实现中成长起来,并进行的广泛范围内分享,当然,它们有时,并不一定,会采用开源模式。现在开源的做法也变得越来越普遍,git或者github成为了它们事实上的版本控制系统。

Netfix就是这样的一个组织,它是非常好的一个例子。分享有用的、尤其是经过实践的代码库激励着其它的开发着也使用相似的方式来解决相似的问题,当然,也保留着根据需要使用不同的方法的权力。共享库更关注于数据存储、进程内通信以及我们接下来做讨论到的自动化等这些问题上。

微服务社区中,开销问题特别引人注意。这并不是说,社区不认为服务交互的价值。相反,正是因为发现到它的价值。这使得他们在寻找各种方法来解决它们。如[Tolearant Reader](http://martinfowler.com/bliki/TolerantReader.html)和[Consumer-Driven Contracts](http://martinfowler.com/articl ... s.html)这样的设计模式就经常被微服务使用。这些模式解决了独立服务在交互过程中的消耗问题。使用Consumer-Driven Contracts增加了你的信心,并实现了快速的反馈机制。事实上,我们知道澳大利亚的一个团队致力使用Consumer-Drvien Contracts开发新的服务。他们使用简单的工程,帮助他们定义服务的接口。使得在新服务的代码开始编写之前,这些接口就成为自动化构建的一个部分。构建出来的服务,只需要指出这些接口适用的范围,一个优雅的方法避免了新软件中的'YAGNI '困境。这些技术和工具在使用过程中完善,通过减少服务间的耦合,限制了集中式管理的需求。

也许分散治理普及于亚马逊“编译它,运维它”的理念。团队为他们开发的软件负全部责任,也包含7*24小时的运行。全责任的方式并不常见,但是我们确实发现越来越多的公司在他们的团队中所推广。Netfix是另外一个接受这种理念的组件。每天凌晨3点被闹钟吵醒,因为你非常的关注写的代码质量。这在传统的集中式治理中这是一样多么不思议的事情呀。

分散数据管理

对数据的分散管理有多种不同的表现形式。最为抽象层次,它意味着不同系统中的通用概念是不同的。这带来的觉问题是大型的跨系统整合时,用户使用不同的售后支持将得到不同的促销信息。这种情况叫做并没有给用户显示所有的促销手段。不同的语法确实存在相同的词义或者(更差)相同的词义。

应用之间这个问题很普遍,但应用内部这个问题也存在,特别是当应用拆分成不同的组件时。对待这个问题非常有用的方式为[Bounded Context](http://martinfowler.com/bliki/BoundedContext.html)的领域驱动设计。DDD把复杂的领域拆分成不同上下文边界以及它们之间的关系。这样的过程对于整体架构和微服务框架都很有用,但是服务间存在着明显的关系,帮助我们对上下文边界进行区分,同时也像我们在业务功能中谈到的,强行拆分。

当对概念模式下决心进行分散管理时,微服务也决定着分散数据管理。当整体式的应用使用单一逻辑数据库对数据持久化时,企业通常选择在应用的范围内使用一个数据库,这些决定也受厂商的商业权限模式驱动。微服务让每个服务管理自己的数据库:无论是相同数据库的不同实例,或者是不同的数据库系统。这种方法叫[Polyglot Persistence](http://martinfowler.com/bliki/ ... e.html)。你可以把这种方法用在整体架构中,但是它更常见于微服务架构中。

microservices-translate-4.png

图 4 Polyglot Persistence

微服务音分散数据现任意味着管理数据更新。处理数据更新的常用方法是使用事务来保证不同的资源修改数据库的一致性。这种方法通常在整体架构中使用。

使用事务是因为它能够帮助处理一至性问题,但对时间的消耗是严重的,这给跨服务操作带来难题。分布式事务非常难以实施,因此微服务架构[强调服务间事务的协调](http://www.eaipatterns.com/ram ... s.html),并清楚的认识一致性只能是最终一致性以及通过补偿运算处理问题。

选择处理不一致问题对于开发团队来说是新的挑战,但是也是一个常见的业务实践模式。通常业务上允许一定的不一致以满足快速响应的需求,但同时也采用一些恢复的进程来处理这种错误。当业务上处理强一致性消耗比处理错误的消耗少时,这种付出是值的的。

基础设施自动化

基础设施自动化技术在过去几年中得到了长足的发展:云计算,特别是AWS的发展,减少了构建、发布、运维微服务的复杂性。

许多使用微服务架构的产品或者系统,它们的团队拥有丰富的[持集部署](http://martinfowler.com/bliki/ ... y.html)以及它的前任[持续集成](http://martinfowler.com/articl ... n.html)的经验。团队使用这种方式构建软件致使更广泛的依赖基础设施自动化技术。下图说明这种构建的流程:

microservices-translate-5.png

图 5 基本的构建流程

尽管这不是介绍自动部署的文章,但我们也打算介绍一下它的主要特征。我们希望我们的软件应该这样方便的工作,因此我们需要更多的自动化测试。流程中工作的软件改进意味着我们能自动的部署到各种新的环境中。

整体风格的应用相当开心的在各种环境中构建、测试、发布。事实证明,一旦你打算投资一条整体架构应用自动化的的生产线,那么你会发现发布更多的应用似乎非不那么的可怕。记住,CD(持续部署)的一个目标在于让发布变得无趣,因此无论是一个还是三个应用,它都一样的无趣。

另一个方面,我们发现使用微服务的团队更加依赖于基础设施的自动化。相比之下,在整体架构也微服务架构中,尽管发布的场景不同,但发布工作的无趣并没有多大的区别。

microservices-translate-6.png

图 6 模块化部署的区别

容错性设计

使用服务作为组件的一个结果在于应用需要有能容忍服务的故障的设计。任务服务可能因为供应商的不可靠而故障,客户端需要尽可能的优化这种场景的响应。跟整体构架相比,这是一个缺点,因为它带来的额外的复杂性。这将让微服务团队时刻的想到服务故障的情况下用户的体验。Netflix 的[Simian Army](https://github.com/Netflix/SimianArmy)可以为每个应用的服务及数据中心提供日常故障检测和恢复。

这种产品中的自动化测试可以让大部分的运维团队正常的上下班。这并不意味着整体构架的应用没有这么精巧的监控配置,只是在我们的经验中它并不常见。

由于服务可以随时故障,快速故障检测,乃至,自动恢复变更非常重要。微服务应用把实时的监控放在应用的各个阶段中,检测构架元素(每秒数据库的接收的请求数)和业务相关的指标(把分钟接收的定单数)。监控系统可以提供一种早期故障告警系统,让开发团队跟进并调查。

对于微服务框架来说,这相当重要,因为微服务相互的通信可能导致紧急意外行为。许多专家车称赞这种紧急事件的价值,但事实是这种紧急行为有时是灾难。监控是至关重要的,它能快速发现这种紧急不良行为,让我们迅速修复它。

整体架构,跟微服务一样,在构建时是通明的,实情上,它们就是这样子的。它们不同之处在于,你需要清楚的认识到不同进程间运行的服务是不相关的。库对于同一进程是透明的,也因此不那么重要了。

微服务团队期望清楚的监控和记录每个服务的配置,比如使用仪表盘显示上/下线状态、各种运维和业务相关的指标。对断路器(circuit breaker)状态、目前的吞吐量和时延细节,我们也会经常遇到。

设计改进

微服务实践者,通常有不断改进设计的背景,他们把服务分解成进一步的工具。这些工具可以让应用开发者在不改变速度情况下,控制都他们的应用的需求变更。变更控制不意味首减少变更,而是使用适当的方式和工具,让它更加频繁,至少,很好让它变得可控。

不论如何,当你试图软件系统拆分成组件时,你将面临着如何拆分的问题。那么我们的决定拆分我们应用的原则是什么呢?首要的因素,组件可以被独立替换和更新的,这意味着我们寻找的关键在于,我们要想象着重写一个组件而不影响它们之前的协作关系。事实上,许多的微服务小组给它进一步的预期:服务应该能够报废的,而不是要长久的发展的。

Guardian网站就是这方面的一个优秀的例子,它初期被设计和构建成一个整体架构,但它已经向微服务的发展了。整体构架仍然是它网站的核心,但是他们使用微服务来增加那些使用整体架构API的新特性。这种方法增加这些临时的特性非常方便,比如运动新闻的特稿。这样站点的一个部分可以使用快速的开发语言迅速整合起来,当它过时后可以一次性移除。我们发现一家金融机构用相似的方法增加新的市场营销活动,数周或者几个月后把它撤销。

可代替是模块化开发中的一个特例,它是用模块来应对需要变更的。你希望让变更是相同模块,相同周期中进行变化而已。系统的某些很小做变更部分,也应该放在不同的服务中,这样它们更容易让它们消亡。如果你发现两个服务一直重复的变更时,这就是一个要合并它们的信号了。

把组件改成服务,增加了细化发布计划的一个机会。整体构架的任务变更需要整个应用的完整的构建和发布。然而,使用微服务,你只需要发布你要修改的服务就可以了。这将简化和加速你的发布周期。缺点是你需要为一个变更服务发布可能中断用户的体验而担心。传统的集成方法是使用版本来处理这些问题,但是微服务[版本仅是最后的通告手段](http://martinfowler.com/articl ... ioning)。我们需要在设计服务时尽可能的容忍供应商的变更,以避免提供多个版本。

微服务是未来吗?

我们写这篇文章的主要目的在于解释微服务的主要思想和原则。但是发时间做这事的时候,我们清醒的认识到微服务构架风格是一个非常重要的想法:一个值得企业应用中认真考虑的东西。我们最近使用这种风格构建了几个系统,认识那些也使用和喜欢这种方法的爱好者。

我们认识的使用这种方式的先行者,包含亚马逊、Netflix、The Guardian、The UK Government Digital Service、realestate.com.au、Forward和comparethemarket.com。2013看的巡回会议充满了向正在想成为微服务一分子的公司,包含Travis CI。此外,大量的组件正在从事我们认为是微服务的事,只是没有使用微服务的名字而已。(通常,它们被打上SOA的标签,尽管,我们认为SOA有许多不同的地方。)

尽管有这些积极的经验,然后,我们也不急于确认微服务是未来软件架构方向。至今为止,我们的经验与整体风格的应该中相比出来的是有优势的,但是我们意识知这样的事实,我们并没有足够的时间来证明我们的论证。

你所使用的架构通常是你开发出来后,使用的几年的实际成果。我们看到这些工程是在一个优秀的团队,带着对模块化的强烈追求,使用在过去几年中已经衰退的整体架构构建出来的。许多人相信,这种衰退不太可能与微服务有关,因为服务边界是清晰的并且很难再完善的。然而,当我们还没看到足够多的系统运行足够长时间时,我们不能肯定微服务构架是成熟的。

当然,还有原因就是,有人期望微服务构架不够成熟。在组件化方面的任何努力,其成功都依赖于软件如何拆分成适合的组件。指出组件化的准确边界应该在那,这是非常困难的。改良设计要承认边界的权益困境和因此带来的易于重构的重要性。但是当你的组件是被远程通信的服务时,重构比进程内的库又要困难的多。服务边界上的代码迁移是困难的,任务接口的变更需要参与者的共同协作,向后兼容的层次需要被增加,测试也变更更加复杂。

另一个问题在于,如果组件并没有清晰的划分,你的工作的复杂性将从组件内部转向组件间的关系。做这事不仅要围绕着复杂,它也要面对着不清晰和更难控制的地方。很容易想到,当你在一个小的、简单的组件内找东西,总比在没有关系的混乱的服务间要容易。

最后,团队技能也是重要的因素。新的技术倾向于被掌握更多的技能的团队使用。但是掌握多技能的团队中使用的技巧在较少技能的团队中并不是必需的。我们发现大量的少技能的团队构建混乱的整合构架,但是它要发时间去证明使用微服务在这种情况下会发生什么。一个糟糕的团队通常开发糟糕的系统:很难说,微服务在这种情况下是否能帮助它们,还是破坏它们。

  一个理性的争议在于,我们听说,你不应该从微服务构架开始做。最好从整体构架开发,做模块化开发,然后当整体构架出现问题是再把模块化拆分成服务。(尽管这种建议不是好主意,因为一个好的进程内接口并不是一个好的服务接口。)

因此我们持这种谨慎的乐观。到目前为止,我们还没有足够认识,关于微构架能否被大范围的推广。我们不能肯定的说,我们要终结什么,但是软件开发的挑战在于你只能在不完整的信息中决定你目前要处理的问题。

其它

微服务系统多大?

尽管“微服务”一词在架构风格中越来越流行,它的名字很不辛让人关注它的服务大小,以及对“微”这个组成的争议。在我们与微服务实践者的谈话中,我们发现了服务的大小范围。被报道的最大团队遵循亚马逊Tow Pizaa团队理念(比如,一个团队吃两个比萨就可以了。),这意味着不超过20号(一打)人。我们发现最小配置是半打的团队支撑起一打的服务。

这也引发这样的考虑:规模为一个服务一打人到一个服务一个人的团队打上微服务的标签。此刻我们认为,它们是一样的,但是随着对这种风格的深入研究,也存在我们改变我们的想法的可能。

微服务与SOA

当前我们谈到微服务时,通常会问,这是不是我们20年前讨论的面向服务架构(SOA)。这是一个很好的观点,因为微服务风格也SOA所提倡的一些优势非常相似。尽管如此,问题在于SOA意味的[太多不同的东西](http://martinfowler.com/bliki/ ... y.html)了,因此通常时候我们谈的所谓“SOA”时,它与我们谈论的风格不一至,因为它通常是指在整体风格应用中的ESB。

此外,我们发现面向服务的风格是这么的拙劣:从试图使用ESB隐藏复杂性, 到使用多年才认识到发费数百美元却没产生任务价值这样的失败,到集中治理模式抑制变更。而且这些问题往往很难发现。

可以肯定的时,微服务社区中使用的许多的技术都开发者是从大型机构的整合服务经验中发展来的。[Tolerant Reader](http://martinfowler.com/bliki/TolerantReader.html)模式就是这样的一个例子。由于互联网的发展,利用简单的协议这种方法,让它从这些经验传达的出来。这是从已经很复杂的集中式标准中的一种反模式,坦白的说,真让人惊叹。(无论何时,当你需要用一个服务来管理你的所有的服务,你就知道这很麻烦。)

SOA的这种常见行为让微服务的提倡者拒绝打上SOA的标签,尽管有人认为微服务是从SOA中发展而来的,或许面向服务是对的。无论如何,事实上SOA表达这么多的含义,它给一个团队清醒的认识到这种构架风格就已经值的了。

多语言,多选择

JVM做为一个平台,它的增长就是一个平台中运行多语言的最大的例子。过去二十年中,它通常做为更高层次语言的壳,以达到更高层次的抽象。比如,研究它的内部结构,、使用低级的语言写更高效的代码。尽管如此,许多整体风格并不需要这种层次的性能优化或者在语法及高层次上的抽象,这很常见(让我们很失望)。此外整体构架通常意味着使用单一的语言,这也限制着使用技术的数量。

实践标准和强制标准

它有点尴尬,微服务团队倾向于避免这种通常由企业架构队伍定制的僵硬的强制标准,但是它们却非常乐于甚至推广这些开放的标准,如HTTP、ATOM、其它微规范。

关键的不同在这些标准是怎么开发出来的,以及它们是怎么被推广的。标准被一些组件管理,如IETF认证标准,仅当它们在互联网上有几个在用的实现,通常源自于开源工程的成功应用。

这些标准单独分离出来,与那种在企业中通常有没有什么编码经验的或者没有什么影响力的厂商标准进行区别。

让做对事更容易

一方面,我们发现在持续发布、部署越来越多的使用自动化,是很多有用的工具开发出来帮助开发者和运营商的努力结果。为打包、代码管理、支撑服务的工具,或者增加标准监控的记录的工具,现在都非常常见了。网络中最好的,可能就是[Netflix's的开源工具](http://netflix.github.io/),但是包含[Dropwizard](http://dropwizard.codahale.com/)在内的其它工具也被广泛的使用着。

断路器(circuit breaker)和产品中现有的代码

[断路器](http://martinfowler.com/bliki/CircuitBreaker.html)(circuit breaker)出现在《[Realease It!](http://www.amazon.com/gp/produ ... 32NXZO)》一书中,与Bulkhead和Timeout这样的模式放在一起。实施起来,这些模式用于构建通信应用时相当的重要。[Netflix的博客](http://techblog.netflix.com/20 ... e.html)在解释它们的应用时,做了大量的工作。

同步是有害的

任务时候,你在服务间的调用使用同步的方法,都会遇到宕机时间的乘积效应。简单的说,你的系统宕机时间是你系统的单独组件的宕机时间的乘积。你面临的选择使用异步或者管理宕机时间。在[www.guardian.co.uk](http://www.guardian.co.uk/)中,它们在新平台中使用一种简单的规则来实现它:在Netflix中每次用户请求的同步调用,他们重新设计的平台API都会把它构建成异步的API来执行。


本文作者:船长&CAP
原文链接:http://www.cnblogs.com/liuning8023/p/4493156.html
版权归作者所有,转载请注明出处


微服务架构中服务端与客户端如何编码

回复

jackie 发起了问题 • 1 人关注 • 0 个回复 • 299 次浏览 • 2017-06-20 14:22 • 来自相关话题

微服务初级设计指南

梁桂钊 发表了文章 • 14 个评论 • 909 次浏览 • 2017-06-20 07:05 • 来自相关话题

如何拆分服务

微服务要如何拆分,是否拆分粒度越小越好?一般情况下,对于服务的拆分并非越小越好,甚至极端的案例是把一块功能拆分成一个服务,这种做法是不对的。因此,拆分粒度应该保证微服务具有业务的独立性与完整性,服务的拆分围绕业务模块进行拆分。例如将 VR 资讯系统进行服务拆分,分为资讯系统、话题系统、日报系统、百科系统四个微服务系统。






但是,很多情况下,服务的拆分围绕业务模块进行拆分是一种理想状态下的拆分方法,换句话说,我们在架构设计之初就假定我们可以掌握一切。然而,不同的服务可能由不同的团队开发与维护,实际场景下,微服务的便利性更多的在于团队内部能够产生闭环,换句话说,团队内部可以易于开发与维护,便于沟通与协作,但是对于外部团队就存在很大的沟通成本与协作成本。现在,我们来看一个案例。团队 A 考虑到功能的复用性而开发了一个“互动组件”,其中包括 “评论模块”功能。此时,团队 B 并不知情也开发了一个类似的“互动组件”。而团队 C 也有这个需求,它知道团队 A 有这个“互动组件”,希望可以复用,但是由于这个“互动组件”在设计的时候更多地考虑了团队 A 的当前业务,没有很好的复用性,例如不支持“评论盖楼”功能,而由于团队 A 出于当前其他项目的进度原因无法马上提供支持,团队 B 评估后决定花一周时间自己开发一个符合自己业务需求的“互动组件”。此时,各个项目团队各自维护了一个“互动组件”。此外,我们再来看一个案例。一个 OA 系统拥有“用户管理”、“文件管理”、“公告管理”、“政策管理”、“公文管理”、“任务管理”、“审批管理”等功能,如果按照微服务架构思想可以围绕业务模块进行拆分,但是事实上这个 OA 系统的最终用户只有 30 多人,使用微服务架构可能有点“杀鸡用牛刀”的感觉了。回顾下,第一个案例中,由于团队之间的职责与边界导致了服务的复用存在局限性,甚至造成各自为战的局面,这种情况一般需要公司层面进行规划和统筹。第二案例中,由于用户量不大,系统也不复杂,使用微服务反而带来了不必要的设计和运维难度,同时也带来了一些技术的复杂度。此外,我们还需要考虑服务依赖,链式调用、数据一致性、分布式事务等问题。

总结下,服务的拆分是一个非常有学问的技术活,要围绕业务模块进行拆分,拆分粒度应该保证微服务具有业务的独立性与完整性,尽可能少的存在服务依赖,链式调用。但是,在实际开发过程中,有的时候单体架构更加适合当前的项目。实际上,微服务的设计并不是一蹴而就的,它是一个设计与反馈过程。因此,我们在设计之初可以将服务的粒度设计的大一些,并考虑其可扩展性,随着业务的发展,进行动态地拆分也是一个不错的选择。

论微服务的数据库管理

通常情况下,在每个服务都有自己的缓存和数据库,并且缓存和数据库是相互独立且透明的。因此,共享缓存与共享数据库是不对的。那如果服务 A 需要获取服务 B 的数据怎么办?请读者思考,这种情况下,可以直接在服务 A 创建两个数据源(一个是服务 A 的数据库,一个是服务 B 的数据库)进行数据操作么?事实上,这个方案是不提倡的,因为它破坏了微服务之间的数据独立性。因此,更好的做法是:服务 B 提供一个获取该数据的 API 接口,而服务 A 通过调用该接口进行业务组装。但是,凡事无绝对,有一种特殊的场景可能需要共享数据库,那就是旧的服务过度到新的服务的场景,新的服务复用旧的服务的数据库从而到达功能与数据过度的需求。

服务多版本指南

微服务的 API 接口应该尽量兼容之前的版本,换句话说,微服务可以支持多版本,但是需要尽量确保向下兼容。如果服务无法兼容旧版本,则需要升级版本号。为了解决这个版本不兼容问题,在设计 RESTful API 的一种实用的做法是使用版本号。一般情况下,我们会在 url 中保留版本号,并同时兼容多个版本。【GET】 /v1/users/{user_id} // 版本 v1 的查询用户列表的 API 接口
【GET】 /v2/users/{user_id} // 版本 v2 的查询用户列表的 API 接口
在 Java 语言的 Spring 框架中实现,如下所示。@RestController
@RequestMapping({"v1/c/users"})
public class SysUserV1Controller {
@RequestMapping(value={"/{userId:\\d+}"}, method=RequestMethod.GET)
public SysUser findOne(@PathVariable long userId){
// 业务实现
}
}

@RestController
@RequestMapping({"v2/c/users"})
public class SysUserV2Controller {
@RequestMapping(value={"/{userId:\\d+}"}, method=RequestMethod.GET)
public SysUser findOne(@PathVariable long userId){
// 业务实现
}
}
此时,客户端的产品的新功能将请求新的服务端的 API 接口地址。






虽然服务端会同时兼容多个版本,但是同时维护太多版本对于服务端而言是个不小的负担,因为服务端要维护多套代码。这种情况下,常见的做法不是维护所有的兼容版本,而是只维护最新的几个兼容版本,例如维护最新的三个兼容版本。在一段时间后,当绝大多数用户升级到较新的版本后,废弃一些使用量较少的服务端的老版本API 接口版本,并要求使用产品的非常旧的版本的用户强制升级。

此外,在微服务的多版本并存的场景下,这些服务的版本可以在同一个服务中定义,这种情况一般出现在服务的内容变更不是很大的场景。另一种方式,服务的版本可以在不同的服务中定义,例如“资讯v1服务”、“资讯v2服务”, 这种情况一般出现在服务框架重构中,它将抽离出一个新的工程来提供新的服务接口。

应对微服务的链式调用异常

一般情况下,每个微服务之间是独立的,如果某个服务宕机,只会影响到当前服务,而不会对整个业务系统产生影响。但是,服务端可能会在多个微服务之间产生一条链式调用,并把整合后的信息返回给客户端。在调用过程中,如果某个服务宕机或者网络不稳定可能造成整个请求失败。因此,为了应对微服务的链式调用异常,我们需要在设计微服务调用链时不宜过长,以免客户端长时间等待,以及中间环节出现错误造成整个请求失败。此外,可以
考虑使用消息队列进行业务解耦,并且使用缓存避免微服务的链式调用从而提高该接口的可用性。

是否需要提供外观接口

外观接口,指的是将多个服务的接口进行业务封装与整合并提供一个简单的调用接口给客户端使用。这种设计的好处在于,客户端不再需要知道那么多服务的接口,只需要调用这个外观接口即可。但是,坏处也是显而易见的,即增加了服务端的业务复杂度,接口性能不高,并且复用性不高。因此,笔者的建议是服务端的接口设计尽可能保证职责单一,而在客户端进行“乐高式”组装。如果存在 SEO 优化的产品,需要被类似于百度这样的搜索引擎收录,可以当首屏的时候,通过服务端渲染生成 HTML,使之让搜索引擎收录,若不是首屏的时候,可以通过客户端调用服务端 RESTful API 接口进行页面渲染。

如何快速追踪与定位问题

在微服务复杂的链式调用中,我们会比单体架构更难以追踪与定位问题。因此,在设计的时候,需要特别注意。一种比较好的方案是,当 RESTful API 接口出现非 2xx 的 HTTP 错误码响应时,采用全局的异常结构响应信息。其中,code 字段用来表示某类错误的错误码,在微服务中应该加上“{biz_name}/”前缀以便于定位错误发生在哪个业务系统上。我们来看一个案例,假设“用户中心”某个接口没有权限获取资源而出现错误,我们的业务系统可以响应“UC/AUTH_DENIED”,并且通过自动生成的 UUID 值的 request_id 字段,在日志系统中获得错误的详细信息。HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"code": "INVALID_ARGUMENT",
"message": "{error message}",
"cause": "{cause message}",
"request_id": "01234567-89ab-cdef-0123-456789abcdef",
"host_id": "{server identity}",
"server_time": "2014-01-01T12:00:00Z"
}
此外,我们需要在记录日志时,标记出错误来源以及错误详情便于更好地分析与定位问题。

微服务的安全

OAuth 是一个关于授权的开放网络标准,它允许第三方网站在用户授权的前提下访问用户在服务商那里存储的各种信息。实际上,OAuth 2.0 允许用户提供一个令牌给第三方网站,一个令牌对应一个特定的第三方网站,同时该令牌只能在特定的时间内访问特定的资源。用户在客户端使用用户名和密码在用户中心获得授权,然后客户端在访问应用是附上 Token 令牌。此时,应用接收到客户端的 Token 令牌到用户中心进行认证。






一般情况下,access token 会添加到 HTTP Header 的 Authorization 参数中使用,其中经常使用到的是 Bearer Token 与 Mac Token。其中,Bearer Token 适用于安全的网络下 API 授权。MAC Token 适用于不安全的网络下 API 授权。

微服务的数据一致性

如何保证多个微服务的数据的一致性是一个必须面对的问题。例如,“话题系统”的数据变更的同时要保证“用户动态系统”的数据级联更新。如果,这个时候“用户动态系统”发生宕机,或者网络连接异常、网络超时,就会导致数据的不一致。目前,分布式事务并没有很好的解决方案,难以满足数据强一致性,一般情况下,保证系统经过一段较短的时间的自我恢复和修正,数据最终达到一致。这个自我恢复和修正的方式,可以在每次更新的时候进行修复,或者采取周期性的进行校验操作来保证。

我们还可以引入可靠的消息队列,只要保证当前“话题系统”的可靠事件投递并且消息中间件确保事件传递至少一次,那么订阅这个事件的消费者(用户动态系统)保证事件能够在自己的业务内被消费即可。在消费者(用户动态系统)处理的过程中出现异常,可以将事件放入重试队列并根据具体的策略进行失败重试,如果多次重试失败可以写入错误日志并主动通知开发人员进行手工介入。注意的是,这个过程要保证可靠事件投递与避免重复消费,其中尤其重要是接口要保证幂等性,例如支付系统不能因为重复收到支付事件而导致多次支付。

此外,我们可以采用业务补偿等方式保证数据的一致性。
 





本文作者: 梁桂钊
原文链接: http://blog.720ui.com/2017/msa_design/
版权归作者所有,转载请注明出处 查看全部
如何拆分服务

微服务要如何拆分,是否拆分粒度越小越好?一般情况下,对于服务的拆分并非越小越好,甚至极端的案例是把一块功能拆分成一个服务,这种做法是不对的。因此,拆分粒度应该保证微服务具有业务的独立性与完整性,服务的拆分围绕业务模块进行拆分。例如将 VR 资讯系统进行服务拆分,分为资讯系统、话题系统、日报系统、百科系统四个微服务系统。

微服务架构示意图.png


但是,很多情况下,服务的拆分围绕业务模块进行拆分是一种理想状态下的拆分方法,换句话说,我们在架构设计之初就假定我们可以掌握一切。然而,不同的服务可能由不同的团队开发与维护,实际场景下,微服务的便利性更多的在于团队内部能够产生闭环,换句话说,团队内部可以易于开发与维护,便于沟通与协作,但是对于外部团队就存在很大的沟通成本与协作成本。现在,我们来看一个案例。团队 A 考虑到功能的复用性而开发了一个“互动组件”,其中包括 “评论模块”功能。此时,团队 B 并不知情也开发了一个类似的“互动组件”。而团队 C 也有这个需求,它知道团队 A 有这个“互动组件”,希望可以复用,但是由于这个“互动组件”在设计的时候更多地考虑了团队 A 的当前业务,没有很好的复用性,例如不支持“评论盖楼”功能,而由于团队 A 出于当前其他项目的进度原因无法马上提供支持,团队 B 评估后决定花一周时间自己开发一个符合自己业务需求的“互动组件”。此时,各个项目团队各自维护了一个“互动组件”。此外,我们再来看一个案例。一个 OA 系统拥有“用户管理”、“文件管理”、“公告管理”、“政策管理”、“公文管理”、“任务管理”、“审批管理”等功能,如果按照微服务架构思想可以围绕业务模块进行拆分,但是事实上这个 OA 系统的最终用户只有 30 多人,使用微服务架构可能有点“杀鸡用牛刀”的感觉了。回顾下,第一个案例中,由于团队之间的职责与边界导致了服务的复用存在局限性,甚至造成各自为战的局面,这种情况一般需要公司层面进行规划和统筹。第二案例中,由于用户量不大,系统也不复杂,使用微服务反而带来了不必要的设计和运维难度,同时也带来了一些技术的复杂度。此外,我们还需要考虑服务依赖,链式调用、数据一致性、分布式事务等问题。

总结下,服务的拆分是一个非常有学问的技术活,要围绕业务模块进行拆分,拆分粒度应该保证微服务具有业务的独立性与完整性,尽可能少的存在服务依赖,链式调用。但是,在实际开发过程中,有的时候单体架构更加适合当前的项目。实际上,微服务的设计并不是一蹴而就的,它是一个设计与反馈过程。因此,我们在设计之初可以将服务的粒度设计的大一些,并考虑其可扩展性,随着业务的发展,进行动态地拆分也是一个不错的选择。

论微服务的数据库管理

通常情况下,在每个服务都有自己的缓存和数据库,并且缓存和数据库是相互独立且透明的。因此,共享缓存与共享数据库是不对的。那如果服务 A 需要获取服务 B 的数据怎么办?请读者思考,这种情况下,可以直接在服务 A 创建两个数据源(一个是服务 A 的数据库,一个是服务 B 的数据库)进行数据操作么?事实上,这个方案是不提倡的,因为它破坏了微服务之间的数据独立性。因此,更好的做法是:服务 B 提供一个获取该数据的 API 接口,而服务 A 通过调用该接口进行业务组装。但是,凡事无绝对,有一种特殊的场景可能需要共享数据库,那就是旧的服务过度到新的服务的场景,新的服务复用旧的服务的数据库从而到达功能与数据过度的需求。

服务多版本指南

微服务的 API 接口应该尽量兼容之前的版本,换句话说,微服务可以支持多版本,但是需要尽量确保向下兼容。如果服务无法兼容旧版本,则需要升级版本号。为了解决这个版本不兼容问题,在设计 RESTful API 的一种实用的做法是使用版本号。一般情况下,我们会在 url 中保留版本号,并同时兼容多个版本。
【GET】 /v1/users/{user_id} // 版本 v1 的查询用户列表的 API 接口
【GET】 /v2/users/{user_id} // 版本 v2 的查询用户列表的 API 接口

在 Java 语言的 Spring 框架中实现,如下所示。
@RestController
@RequestMapping({"v1/c/users"})
public class SysUserV1Controller {
@RequestMapping(value={"/{userId:\\d+}"}, method=RequestMethod.GET)
public SysUser findOne(@PathVariable long userId){
// 业务实现
}
}

@RestController
@RequestMapping({"v2/c/users"})
public class SysUserV2Controller {
@RequestMapping(value={"/{userId:\\d+}"}, method=RequestMethod.GET)
public SysUser findOne(@PathVariable long userId){
// 业务实现
}
}

此时,客户端的产品的新功能将请求新的服务端的 API 接口地址。

服务多版本示意图.jpg


虽然服务端会同时兼容多个版本,但是同时维护太多版本对于服务端而言是个不小的负担,因为服务端要维护多套代码。这种情况下,常见的做法不是维护所有的兼容版本,而是只维护最新的几个兼容版本,例如维护最新的三个兼容版本。在一段时间后,当绝大多数用户升级到较新的版本后,废弃一些使用量较少的服务端的老版本API 接口版本,并要求使用产品的非常旧的版本的用户强制升级。

此外,在微服务的多版本并存的场景下,这些服务的版本可以在同一个服务中定义,这种情况一般出现在服务的内容变更不是很大的场景。另一种方式,服务的版本可以在不同的服务中定义,例如“资讯v1服务”、“资讯v2服务”, 这种情况一般出现在服务框架重构中,它将抽离出一个新的工程来提供新的服务接口。

应对微服务的链式调用异常

一般情况下,每个微服务之间是独立的,如果某个服务宕机,只会影响到当前服务,而不会对整个业务系统产生影响。但是,服务端可能会在多个微服务之间产生一条链式调用,并把整合后的信息返回给客户端。在调用过程中,如果某个服务宕机或者网络不稳定可能造成整个请求失败。因此,为了应对微服务的链式调用异常,我们需要在设计微服务调用链时不宜过长,以免客户端长时间等待,以及中间环节出现错误造成整个请求失败。此外,可以
考虑使用消息队列进行业务解耦,并且使用缓存避免微服务的链式调用从而提高该接口的可用性。

是否需要提供外观接口

外观接口,指的是将多个服务的接口进行业务封装与整合并提供一个简单的调用接口给客户端使用。这种设计的好处在于,客户端不再需要知道那么多服务的接口,只需要调用这个外观接口即可。但是,坏处也是显而易见的,即增加了服务端的业务复杂度,接口性能不高,并且复用性不高。因此,笔者的建议是服务端的接口设计尽可能保证职责单一,而在客户端进行“乐高式”组装。如果存在 SEO 优化的产品,需要被类似于百度这样的搜索引擎收录,可以当首屏的时候,通过服务端渲染生成 HTML,使之让搜索引擎收录,若不是首屏的时候,可以通过客户端调用服务端 RESTful API 接口进行页面渲染。

如何快速追踪与定位问题

在微服务复杂的链式调用中,我们会比单体架构更难以追踪与定位问题。因此,在设计的时候,需要特别注意。一种比较好的方案是,当 RESTful API 接口出现非 2xx 的 HTTP 错误码响应时,采用全局的异常结构响应信息。其中,code 字段用来表示某类错误的错误码,在微服务中应该加上“{biz_name}/”前缀以便于定位错误发生在哪个业务系统上。我们来看一个案例,假设“用户中心”某个接口没有权限获取资源而出现错误,我们的业务系统可以响应“UC/AUTH_DENIED”,并且通过自动生成的 UUID 值的 request_id 字段,在日志系统中获得错误的详细信息。
HTTP/1.1 400 Bad Request
Content-Type: application/json
{
"code": "INVALID_ARGUMENT",
"message": "{error message}",
"cause": "{cause message}",
"request_id": "01234567-89ab-cdef-0123-456789abcdef",
"host_id": "{server identity}",
"server_time": "2014-01-01T12:00:00Z"
}

此外,我们需要在记录日志时,标记出错误来源以及错误详情便于更好地分析与定位问题。

微服务的安全

OAuth 是一个关于授权的开放网络标准,它允许第三方网站在用户授权的前提下访问用户在服务商那里存储的各种信息。实际上,OAuth 2.0 允许用户提供一个令牌给第三方网站,一个令牌对应一个特定的第三方网站,同时该令牌只能在特定的时间内访问特定的资源。用户在客户端使用用户名和密码在用户中心获得授权,然后客户端在访问应用是附上 Token 令牌。此时,应用接收到客户端的 Token 令牌到用户中心进行认证。

Oauth2_协议的授权与认证.jpg


一般情况下,access token 会添加到 HTTP Header 的 Authorization 参数中使用,其中经常使用到的是 Bearer Token 与 Mac Token。其中,Bearer Token 适用于安全的网络下 API 授权。MAC Token 适用于不安全的网络下 API 授权。

微服务的数据一致性

如何保证多个微服务的数据的一致性是一个必须面对的问题。例如,“话题系统”的数据变更的同时要保证“用户动态系统”的数据级联更新。如果,这个时候“用户动态系统”发生宕机,或者网络连接异常、网络超时,就会导致数据的不一致。目前,分布式事务并没有很好的解决方案,难以满足数据强一致性,一般情况下,保证系统经过一段较短的时间的自我恢复和修正,数据最终达到一致。这个自我恢复和修正的方式,可以在每次更新的时候进行修复,或者采取周期性的进行校验操作来保证。

我们还可以引入可靠的消息队列,只要保证当前“话题系统”的可靠事件投递并且消息中间件确保事件传递至少一次,那么订阅这个事件的消费者(用户动态系统)保证事件能够在自己的业务内被消费即可。在消费者(用户动态系统)处理的过程中出现异常,可以将事件放入重试队列并根据具体的策略进行失败重试,如果多次重试失败可以写入错误日志并主动通知开发人员进行手工介入。注意的是,这个过程要保证可靠事件投递与避免重复消费,其中尤其重要是接口要保证幂等性,例如支付系统不能因为重复收到支付事件而导致多次支付。

此外,我们可以采用业务补偿等方式保证数据的一致性。
 
异步消息微服务.jpg


本文作者: 梁桂钊
原文链接: http://blog.720ui.com/2017/msa_design/
版权归作者所有,转载请注明出处


小程聊微服务-增艺眼中的自动化测试

flychao88 发表了文章 • 2 个评论 • 239 次浏览 • 2017-06-15 15:45 • 来自相关话题

如果说“生活不只有眼前的苟且,还有诗和远方”的话,那么自动化测试可以说是很多测试人员心中的“诗和远方”。
 

 
“诗和远方”OR“禁果”

测试自动化,需要持续改进。但由于其本身是一种过于激动人心的想法:用程序去测试程序——解放了测试人员的生产力,节省大量的人力成本,这就有点“禁果”的意思了。

一个常见的行动模式是:在实施自动化测试时,设定一些量化指标,例如根据业务、接口、模块设置的覆盖率。技术团队在完成指标的过程中,以完成指标为中心而忘记了为什么要进行自动化测试。其结果是各项指标都完成了,但是没人能说清自动化测试的效果和价值,而技术人员也没有热情再继续下去。最终,自动化测试只能“被完成”了。

那如何避免自动化测试的诱惑,或者降低诱惑的影响呢?
 
三观,你的三观要正

自动化测试,有开发的过程,有测试的过程,说到底,依然是测试的过程。所以自动化测试,依然遵循测试活动的基本内涵:反馈信息——它是帮助技术团队获得关于软件产品质量信息的反馈手段,这是实施自动化测试的中心。评判自动化测试是否有效的标准,就是自动化是否能够帮助技术团队获取质量信息,其关键就在于自动化测试的测试分析与设计。

没错,自动化测试的测试分析与设计
 
什么?自动化测试也需要进行测试分析与设计吗?不是就把手动执行的用例自动化就可以了吗?

确实,自动化测试实施的一个常见的活动模式是:选择工具或者脚本语言、确定哪些手工执行的用例需要执行,录制或编写执行的脚本,配置测试数据,执行测试。这个模式中就没有自动化的测试分析和设计。而对工具的研究学习通常是开展自动化测试的第一步,特别是对于以往做功能测试的同学而言,掌握工具比测试分析和设计是更显而易见的技能加分项。但工具却不是自动化测试的能否有效的关键。

测试分析与设计是测试活动能否有效的关键,自动化测试也不例外。例如,在数据库表结构变更的场景中,我们需要分析如何确认需要进行验证的数据特征?如何构造这些测试数据?如何验证数据结构变更前后,程序对数据新老数据的处理是符合需求的?这些都可以使用自动化的达到手工测试不同的效果。

但这好像和我们认为的自动化测试不太一样呀。是的,自动化测试并非是将手动执行的用例自动化,它有自己的测试领域,像生成大量的测试数据、多组合的测试数据、发起大量的请求、触发资源竞争、执行严格的结果检查、快速报告结果等等,它在帮助你完成手工难以完成的事,而不是帮你完成手工可以完成的事。你的每一次测试活动,如果有手工无法完成的事,那么你就应该考虑自动化测试,并对自动化进行测试分析和设计。而如果没有手工无法完成的事,我们则需要考虑是不是遗漏了什么。

那把手动执行的用例自动化作为回归测试不可行吗?
 
“啪啪啪”,有可能是打脸

看情况。自动化是否用于回归测试?用例是否自动化?等等,选择一个合适的自动化测试策略,需要关注:测试需求、软件系统结构、人员能力。对于需要从业务的角度进行的回归测试而言,更需要关注产品的迭代节奏或者生命周期。毕竟复杂的业务逻辑、修改已失效的测试的时间成本,通常都会给认为自动化测试能够节省人力成本的人狠狠的一记耳光。

但这并非说业务测试的回归就不能自动化,其关键在于划定业务范围。一个经验是使用“业务逻辑-失效风险”的分析,将业务逻辑按照功能失效后带来的风险进行分析后,圈定高风险业务逻辑进行自动化,严格保证高风险业务的可靠性。另一种方法则可以使用幂律分布,我们常说的2/8原则就是其一个体现:根据生产业务的使用情况,将常用业务逻辑进行自动化,保证其可靠性。

这里我们依然坚持的理念是让自动化帮助我们完成我们手工做不了的,而不是替代手工测试。

“没有两片相同的叶子”,也没有两次相同的手工测试

测试,给人的一个错觉就是重复性,包括有些公司在招测试人员的时候会有一条叫“能够胜任重复性工作”。但试问,谁会手工运行N次相同的测试呢?一个测试人员,他在执行什么测试不重要,他为什么要执行这个测试——背后的分析思考才重要。自动化的测试,能够替代的是相同的执行,却无法替代人的分析与思考。而软件产品在研发迭代的过程,是团队不断地把新的分析思考注入其中的过程。测试工作面对的是始终在不断变化的测试需求。所以测试的分析思考,包括手工和自动化的测试也需要不断地迭代改善。
 
“只有改善,没有成功与失败”

对于有专门自动化测试的小组而言,通常会以项目的形式开展自动化,制定任务的行动计划表或者里程碑等进行跟踪管理。而个人实施自动化,则相对自由宽松。但无论小组还是个人,为了能够保持持续改善的动力,最重要的一点是让每一步工作是有效的,实施团队或个人能够从自动化测试的过程中及时得到正向反馈。对于小组的负责人而言,这是让团队保持动力的关键。一个经验是,任务拆解细化,参考OODA环,周期性进行回顾调整。另一个负责人必须关心的事是,需要周期告知自动化测试的利益相关方目前的情况,例如技术团队、领导层、上下游合作方等。让大家看到一个持续改进的过程,树立对自动化测试的信心。

“生活不只有眼前的苟且,还有远方的苟且”

聊到这,很多人已经感到自动化测试并非是“诗和远方”,更像是“远方的苟且”——没有美感、没有情怀。对,我们所面临的都是一个一个具体的问题,拆开看很挫,很细碎,需要有勇气去面对。

参禅打坐3分钟

无论发生什么都要认真对待,这就是工作的真谛。[1]

[1]摘自《零售的哲学》 查看全部
如果说“生活不只有眼前的苟且,还有诗和远方”的话,那么自动化测试可以说是很多测试人员心中的“诗和远方”。
 

 
“诗和远方”OR“禁果”

测试自动化,需要持续改进。但由于其本身是一种过于激动人心的想法:用程序去测试程序——解放了测试人员的生产力,节省大量的人力成本,这就有点“禁果”的意思了。

一个常见的行动模式是:在实施自动化测试时,设定一些量化指标,例如根据业务、接口、模块设置的覆盖率。技术团队在完成指标的过程中,以完成指标为中心而忘记了为什么要进行自动化测试。其结果是各项指标都完成了,但是没人能说清自动化测试的效果和价值,而技术人员也没有热情再继续下去。最终,自动化测试只能“被完成”了。

那如何避免自动化测试的诱惑,或者降低诱惑的影响呢?
 
三观,你的三观要正

自动化测试,有开发的过程,有测试的过程,说到底,依然是测试的过程。所以自动化测试,依然遵循测试活动的基本内涵:反馈信息——它是帮助技术团队获得关于软件产品质量信息的反馈手段,这是实施自动化测试的中心。评判自动化测试是否有效的标准,就是自动化是否能够帮助技术团队获取质量信息,其关键就在于自动化测试的测试分析与设计。

没错,自动化测试的测试分析与设计
 
什么?自动化测试也需要进行测试分析与设计吗?不是就把手动执行的用例自动化就可以了吗?

确实,自动化测试实施的一个常见的活动模式是:选择工具或者脚本语言、确定哪些手工执行的用例需要执行,录制或编写执行的脚本,配置测试数据,执行测试。这个模式中就没有自动化的测试分析和设计。而对工具的研究学习通常是开展自动化测试的第一步,特别是对于以往做功能测试的同学而言,掌握工具比测试分析和设计是更显而易见的技能加分项。但工具却不是自动化测试的能否有效的关键。

测试分析与设计是测试活动能否有效的关键,自动化测试也不例外。例如,在数据库表结构变更的场景中,我们需要分析如何确认需要进行验证的数据特征?如何构造这些测试数据?如何验证数据结构变更前后,程序对数据新老数据的处理是符合需求的?这些都可以使用自动化的达到手工测试不同的效果。

但这好像和我们认为的自动化测试不太一样呀。是的,自动化测试并非是将手动执行的用例自动化,它有自己的测试领域,像生成大量的测试数据、多组合的测试数据、发起大量的请求、触发资源竞争、执行严格的结果检查、快速报告结果等等,它在帮助你完成手工难以完成的事,而不是帮你完成手工可以完成的事。你的每一次测试活动,如果有手工无法完成的事,那么你就应该考虑自动化测试,并对自动化进行测试分析和设计。而如果没有手工无法完成的事,我们则需要考虑是不是遗漏了什么。

那把手动执行的用例自动化作为回归测试不可行吗?
 
“啪啪啪”,有可能是打脸

看情况。自动化是否用于回归测试?用例是否自动化?等等,选择一个合适的自动化测试策略,需要关注:测试需求、软件系统结构、人员能力。对于需要从业务的角度进行的回归测试而言,更需要关注产品的迭代节奏或者生命周期。毕竟复杂的业务逻辑、修改已失效的测试的时间成本,通常都会给认为自动化测试能够节省人力成本的人狠狠的一记耳光。

但这并非说业务测试的回归就不能自动化,其关键在于划定业务范围。一个经验是使用“业务逻辑-失效风险”的分析,将业务逻辑按照功能失效后带来的风险进行分析后,圈定高风险业务逻辑进行自动化,严格保证高风险业务的可靠性。另一种方法则可以使用幂律分布,我们常说的2/8原则就是其一个体现:根据生产业务的使用情况,将常用业务逻辑进行自动化,保证其可靠性。

这里我们依然坚持的理念是让自动化帮助我们完成我们手工做不了的,而不是替代手工测试。

“没有两片相同的叶子”,也没有两次相同的手工测试

测试,给人的一个错觉就是重复性,包括有些公司在招测试人员的时候会有一条叫“能够胜任重复性工作”。但试问,谁会手工运行N次相同的测试呢?一个测试人员,他在执行什么测试不重要,他为什么要执行这个测试——背后的分析思考才重要。自动化的测试,能够替代的是相同的执行,却无法替代人的分析与思考。而软件产品在研发迭代的过程,是团队不断地把新的分析思考注入其中的过程。测试工作面对的是始终在不断变化的测试需求。所以测试的分析思考,包括手工和自动化的测试也需要不断地迭代改善。
 
“只有改善,没有成功与失败”

对于有专门自动化测试的小组而言,通常会以项目的形式开展自动化,制定任务的行动计划表或者里程碑等进行跟踪管理。而个人实施自动化,则相对自由宽松。但无论小组还是个人,为了能够保持持续改善的动力,最重要的一点是让每一步工作是有效的,实施团队或个人能够从自动化测试的过程中及时得到正向反馈。对于小组的负责人而言,这是让团队保持动力的关键。一个经验是,任务拆解细化,参考OODA环,周期性进行回顾调整。另一个负责人必须关心的事是,需要周期告知自动化测试的利益相关方目前的情况,例如技术团队、领导层、上下游合作方等。让大家看到一个持续改进的过程,树立对自动化测试的信心。

“生活不只有眼前的苟且,还有远方的苟且”

聊到这,很多人已经感到自动化测试并非是“诗和远方”,更像是“远方的苟且”——没有美感、没有情怀。对,我们所面临的都是一个一个具体的问题,拆开看很挫,很细碎,需要有勇气去面对。

参禅打坐3分钟

无论发生什么都要认真对待,这就是工作的真谛。[1]

[1]摘自《零售的哲学》

Martin Fowler关于微服务的原文翻译(一)

flychao88 发表了文章 • 0 个评论 • 237 次浏览 • 2017-06-15 15:43 • 来自相关话题

原文如下:http://martinfowler.com/articl ... .html

微服务

一个新的架构术语

“微服务架构”一词是在过去几年里涌现出来的,它用于描述一种独立部署的软件应用设计方式。这种架构方式并没有非常明确的定义,但有一些共同的特点就是围绕在业务能力、自动化布署、端到端的整合以及语言和数据的分散控制上面。

“微服务”- 这是在软件架构领域这个非常拥挤的街道上,冒出的一个新名词而已。虽然我们对这个新出的名词不屑一顾,但是它所描述的软件系统的风格越来越吸引我们的注意力。在过去的几年里,我们发现越来越多的项目开始使用这个风格,并且到目前为止得到的反馈都是积极的,以至于我身边的许多同事在设计企业架构时,都把它作为默认的构建方式,然而很不幸,到底什么是微服务,我们又如何来使用它,外界并没有太多的信息可供参考。

总之,微服务这种架构风格就是把一组小服务演化成为一个单一的应用的一种方法。每个应用都运行在自己的进程中,并通过轻量级的机制保持通信,就像HTTP这样的API。这些服务要基于业务场景,并使用自动化布署工具进行独立的发布。可以有一个非常轻量级的集中式管理来协调这些服务,可以使用不同的语言来编写服务,也可以使用不同的数据存储。
 
在开始解释什么是微服务之前,先介绍一下单体应用还是很有用的:把一个单体应用构建成为一个部分。企业应用通过是由三个重要部分组成:客户端界面(由HTML、Javascript组成,使用浏览器进行访问)、数据库(由许多的表组件构成一个通用的、相互关联的数据管理系统)、服务端应用。服务端应用处理HTTP请求、执行领域逻辑、检索并更新数据库中的数据、选择和填充HTML视图发送给客户端。这个服务端应用是一个单块结构也就是一个整体,这是一个可执行的单一逻辑,系统中的任何修改都将导致服务端应用重新编译和布署一个新版本。

就这样一个单体应用很自然的被构建成了一个系统,虽然可以使用开发语言基本特性会把应用封装成类、函数、命名空间,但是业务中所有请求都要在单一的进程中处理完成,在某些场景中,你可以在开发人员的笔记本电脑中运行和测试,并且通过布署通道将测试通过的程序布署到生产环境中,你还可以水平扩展,利用负载均衡将实例布署到多台服务器中。

的确,单体应用也是很成功的,但是越来越多的人感觉到了不妥,特别是应用程序被发布到了云的时候,变更发布周期被绑定了 —- 原来可以划分成小的应用、小的需要的变更,需要统一的进行编译和发布。随着时间的推移,软件开发者很难保持原有好的模块架构,使得一个模块的变更很难不会影响到其它的模块,而且在扩展方面也只能进行整体的扩展,而不能根据进行部分的扩展。 
 

 
这些原因导致了微服务架构风格的出现:以服务构建应用。这些服务还可以被独立布署、独立扩展,每个服务也都提供了清晰的模块边界,甚至不同的服务都可以使用不同的编程语言来实现,也可以由不同的团队进行管理。

微服务的概念不是我们发明的,它至少起源于Unix时代的设计原则,我们认为这种风格所带来的好处,并没有引起足够多人的重视。

微服务架构特征

我们没有办法对微服务有一个正式的定义,但我们可以尝试表述适合这种架构的共同特征来给它打上特性标签,共同特性并不代表每个服务都具备这些特点,但是我们真的期望大多数微服务架构能具备其中大部分特点。虽然我们的作者已经是松散社区的核心成员,但是我们也在尝试描述我们工作中或者我们了解的组件中所理解的微服务。我们并不依赖于那些已经明确过的定义。
 
组件化与服务

只要我们一直在从事软件行业,我们的愿望就是,软件由很多组件组装在一起,如同物理现实世界中类似的构造方式。在过去的几十年里,我们已经看到了大部分语言平台公共库有了长足的进步。

当我们在谈论组件时,我们遇到了组件定义方面的困难,我们给定的定义是:一个组件是软件中的一个部分,可以独立的替换和升级。

微服务也会使用组件库,将一个软件组件化的主要方式就是将其分解成服务,我们定义的库是可以连接到程序并使用内存函数的的组件库,服务是进程外的组件,如Web请求服务或者远程调用来相互通信的组件。(这种定义的方式与其它面向对象程序中服务对象的概念是不一样的。)

把服务当成组件(而不是组件库)的一个原因是服务可以独立布署,如果你有一个应用是由多个库组成并且运行在一个进程中,那么任何一点的改变都会引起整个应用的重新发布,但是将这个应用拆解为多个服务,你可以期待每个服务的变更仅需要发布相应的服务就可以,当然这也不是绝对的,比如导致服务接口变更的更新就需要相应服务的变化,但是良好的架构设计是通过聚合服务边界并且按照合约实现服务演化,最大限度地减少因为改变影响其他地方。

把服务当成组件的另一个考虑是这会拥有更加清晰的接口,大多数的语言并没有一个很好的机制来定义一个明确显式的发布接口,通常只有文档和规范说明,让用户避免组件间过度紧密而导致高耦合,通过显示的远程调用机制,可以避免这种情况。
 
使用服务也有其自身的缺点,远程调用比进程内部调用更加消耗性能,而且远程的API往往是粗粒度的,用起来不是很友好,对组件的职责进行变更,也会影响到进程间的交互,那么操作起来也比较困难。

第一个可能性,我们看到每个服务是运行在独立的进程上的。注意,这只是第一个可能性。服务也可以由多个进程组成,它们是同时开发和部署的,如果一个应用进程和一个仅由该服务使用的数据库。

围绕业务能力进行组织

当我们把一个大的应用拆分成小的部分时,我们的注意力主要集中在技术层面,拆分成UI团队、服务端的逻辑团队和数据库团队。当使用这种标准对团队进行划分时,甚至一个非常小的更变都将导致跨团队间项目协作,从而消耗时间和预算审批。一个高效的团队会针对这种情况进行改善,关注它们所涉及的应用逻辑,并从中做出较好的选择。换句话说,逻辑无处不在。康威定律就是一个例子。
 
一个组织的沟通结构反映了其设计的系统的结构 -- Melvyn Conway, 1967
 
 

微服务的划分方法有所不同,它更倾向于围绕业务功能对服务结构进行划分、拆解,这些服务可以采用不同的技术栈来实现,包括用户界面,持久层存储,或任何对外协作,因此团队应该是跨职能的,包括开发所需要的全部技术:用户体验、数据库和项目管理。
 

 
按照这种方式组织的公司是 www.comparethemarket.com,跨职能团队负责建立和操作每个产品并且每个产品都被分成若干单独的服务通过消息进行通信。

大型的单体应用也可以按照业务功能进行模块化的,尽管这种例子不常见。当然,我们也会敦促一个大型团队在构建一个单体应用时按照业务线来进行划分,我们能看到主要问题在于,这种组件形式会导致很多的上下文依赖,如果这个系统跨越很多模块边界,对于一个单独团队是很难在短时间解决问题的。此外,我们发现模块化方式需要大量的规范去强制执行,而服务组件明确的划分,使得团队间的边界也变得清晰起来。

产品不是项目

大多数的开发工作是使用这样一种模型:其目的是完成可以交付的软件,软件开发完成就交给了维护团队,该项目组也就解散了。

微服务的支持者建议避免这种模型,认为一个团队应该负责产品的整个生命周期,一个很通用的概念就是Amazon’s的“you build, you run it”,它要求开发团队对软件产品的整个生命周期负责,这使得开发人员可以每天都关注产品的运行情况,而且也能够与用户保持紧密的联系,做一些必要的支持工作。

产品方式开发意味着与业务能力紧紧捆绑在一起,而不是将软件看成是一系列完成的功能,他们会关注如何让软件帮助其用户提升业务能力。
 
单体应用也可以采用上述产品的理念,但是更小粒度的服务可以更容易的创建开发者与用户之间的关系。

智能终端与弱管道

当在不同的进程之间构建各种通信结构时,我们已经看到许多产品和方法,来强调将大量的智能特性融入到通信机制本身,这种情况的一个典型例子就是“企业服务总线”(Enterprise Service Bus,ESB)。ESB产品经常包括高度智能的设施来进行消息的路由、编排、转换,并应用业务规则。

微服务社区主张采用另一种做法:智能终端和弱管道。使用微服务所构建的各个应用的目标都是尽可能实现“高内聚和低耦合”–他们拥有自己的领域逻辑,并且更多的是经典的UNIX的“过滤器”那样工作–即接收请求、处理逻辑、返回响应,这些应用通过使用简单的REST风格的协议来进行编排,而不去使用复杂的协议,比如:WS、BEPL或者集中式工具进行编排。

有两种协议最经常被使用到:包含资源API的HTTP的请求-响应和轻量级消息通信协议。最为重要的建议为:

微服务团队采用这样的原则和规范:基于互联网(广义上,包含Unix系统)构建系统。这样经常使用的资源几乎不用什么的代价就可以被开发者或者运行商缓存。

第二种做法是通过轻量级消息总线来发布消息。这种的通信协议非常的单一(单一到只负责消息路由),像RabbitMQ或者ZeroMQ这样的简单的实现甚至像可靠的异步机制都没提供,以至于需要依赖产生或者消费消息的终端或者服务来处理这类问题。
 
在一个单块系统中,各个组件在同一个进程中运行。它们相互之间的通信,要么通过方法调用,要么通过函数调用来进行。将一个单块系统改造为若干微服务的最大问题,在于对通信模式的改变。仅仅将内存中的方法调用转换为RPC调用这样天真的做法,会导致微服务之间产生繁琐的通信,使得系统表现变糟。取而代之的是,需要用更粗粒度的协议来替代细粒度的服务间通信。 查看全部
原文如下:http://martinfowler.com/articl ... .html

微服务

一个新的架构术语

“微服务架构”一词是在过去几年里涌现出来的,它用于描述一种独立部署的软件应用设计方式。这种架构方式并没有非常明确的定义,但有一些共同的特点就是围绕在业务能力、自动化布署、端到端的整合以及语言和数据的分散控制上面。

“微服务”- 这是在软件架构领域这个非常拥挤的街道上,冒出的一个新名词而已。虽然我们对这个新出的名词不屑一顾,但是它所描述的软件系统的风格越来越吸引我们的注意力。在过去的几年里,我们发现越来越多的项目开始使用这个风格,并且到目前为止得到的反馈都是积极的,以至于我身边的许多同事在设计企业架构时,都把它作为默认的构建方式,然而很不幸,到底什么是微服务,我们又如何来使用它,外界并没有太多的信息可供参考。

总之,微服务这种架构风格就是把一组小服务演化成为一个单一的应用的一种方法。每个应用都运行在自己的进程中,并通过轻量级的机制保持通信,就像HTTP这样的API。这些服务要基于业务场景,并使用自动化布署工具进行独立的发布。可以有一个非常轻量级的集中式管理来协调这些服务,可以使用不同的语言来编写服务,也可以使用不同的数据存储。
 
在开始解释什么是微服务之前,先介绍一下单体应用还是很有用的:把一个单体应用构建成为一个部分。企业应用通过是由三个重要部分组成:客户端界面(由HTML、Javascript组成,使用浏览器进行访问)、数据库(由许多的表组件构成一个通用的、相互关联的数据管理系统)、服务端应用。服务端应用处理HTTP请求、执行领域逻辑、检索并更新数据库中的数据、选择和填充HTML视图发送给客户端。这个服务端应用是一个单块结构也就是一个整体,这是一个可执行的单一逻辑,系统中的任何修改都将导致服务端应用重新编译和布署一个新版本。

就这样一个单体应用很自然的被构建成了一个系统,虽然可以使用开发语言基本特性会把应用封装成类、函数、命名空间,但是业务中所有请求都要在单一的进程中处理完成,在某些场景中,你可以在开发人员的笔记本电脑中运行和测试,并且通过布署通道将测试通过的程序布署到生产环境中,你还可以水平扩展,利用负载均衡将实例布署到多台服务器中。

的确,单体应用也是很成功的,但是越来越多的人感觉到了不妥,特别是应用程序被发布到了云的时候,变更发布周期被绑定了 —- 原来可以划分成小的应用、小的需要的变更,需要统一的进行编译和发布。随着时间的推移,软件开发者很难保持原有好的模块架构,使得一个模块的变更很难不会影响到其它的模块,而且在扩展方面也只能进行整体的扩展,而不能根据进行部分的扩展。 
 

 
这些原因导致了微服务架构风格的出现:以服务构建应用。这些服务还可以被独立布署、独立扩展,每个服务也都提供了清晰的模块边界,甚至不同的服务都可以使用不同的编程语言来实现,也可以由不同的团队进行管理。

微服务的概念不是我们发明的,它至少起源于Unix时代的设计原则,我们认为这种风格所带来的好处,并没有引起足够多人的重视。

微服务架构特征

我们没有办法对微服务有一个正式的定义,但我们可以尝试表述适合这种架构的共同特征来给它打上特性标签,共同特性并不代表每个服务都具备这些特点,但是我们真的期望大多数微服务架构能具备其中大部分特点。虽然我们的作者已经是松散社区的核心成员,但是我们也在尝试描述我们工作中或者我们了解的组件中所理解的微服务。我们并不依赖于那些已经明确过的定义。
 
组件化与服务

只要我们一直在从事软件行业,我们的愿望就是,软件由很多组件组装在一起,如同物理现实世界中类似的构造方式。在过去的几十年里,我们已经看到了大部分语言平台公共库有了长足的进步。

当我们在谈论组件时,我们遇到了组件定义方面的困难,我们给定的定义是:一个组件是软件中的一个部分,可以独立的替换和升级。

微服务也会使用组件库,将一个软件组件化的主要方式就是将其分解成服务,我们定义的库是可以连接到程序并使用内存函数的的组件库,服务是进程外的组件,如Web请求服务或者远程调用来相互通信的组件。(这种定义的方式与其它面向对象程序中服务对象的概念是不一样的。)

把服务当成组件(而不是组件库)的一个原因是服务可以独立布署,如果你有一个应用是由多个库组成并且运行在一个进程中,那么任何一点的改变都会引起整个应用的重新发布,但是将这个应用拆解为多个服务,你可以期待每个服务的变更仅需要发布相应的服务就可以,当然这也不是绝对的,比如导致服务接口变更的更新就需要相应服务的变化,但是良好的架构设计是通过聚合服务边界并且按照合约实现服务演化,最大限度地减少因为改变影响其他地方。

把服务当成组件的另一个考虑是这会拥有更加清晰的接口,大多数的语言并没有一个很好的机制来定义一个明确显式的发布接口,通常只有文档和规范说明,让用户避免组件间过度紧密而导致高耦合,通过显示的远程调用机制,可以避免这种情况。
 
使用服务也有其自身的缺点,远程调用比进程内部调用更加消耗性能,而且远程的API往往是粗粒度的,用起来不是很友好,对组件的职责进行变更,也会影响到进程间的交互,那么操作起来也比较困难。

第一个可能性,我们看到每个服务是运行在独立的进程上的。注意,这只是第一个可能性。服务也可以由多个进程组成,它们是同时开发和部署的,如果一个应用进程和一个仅由该服务使用的数据库。

围绕业务能力进行组织

当我们把一个大的应用拆分成小的部分时,我们的注意力主要集中在技术层面,拆分成UI团队、服务端的逻辑团队和数据库团队。当使用这种标准对团队进行划分时,甚至一个非常小的更变都将导致跨团队间项目协作,从而消耗时间和预算审批。一个高效的团队会针对这种情况进行改善,关注它们所涉及的应用逻辑,并从中做出较好的选择。换句话说,逻辑无处不在。康威定律就是一个例子。
 
一个组织的沟通结构反映了其设计的系统的结构 -- Melvyn Conway, 1967
 
 

微服务的划分方法有所不同,它更倾向于围绕业务功能对服务结构进行划分、拆解,这些服务可以采用不同的技术栈来实现,包括用户界面,持久层存储,或任何对外协作,因此团队应该是跨职能的,包括开发所需要的全部技术:用户体验、数据库和项目管理。
 

 
按照这种方式组织的公司是 www.comparethemarket.com,跨职能团队负责建立和操作每个产品并且每个产品都被分成若干单独的服务通过消息进行通信。

大型的单体应用也可以按照业务功能进行模块化的,尽管这种例子不常见。当然,我们也会敦促一个大型团队在构建一个单体应用时按照业务线来进行划分,我们能看到主要问题在于,这种组件形式会导致很多的上下文依赖,如果这个系统跨越很多模块边界,对于一个单独团队是很难在短时间解决问题的。此外,我们发现模块化方式需要大量的规范去强制执行,而服务组件明确的划分,使得团队间的边界也变得清晰起来。

产品不是项目

大多数的开发工作是使用这样一种模型:其目的是完成可以交付的软件,软件开发完成就交给了维护团队,该项目组也就解散了。

微服务的支持者建议避免这种模型,认为一个团队应该负责产品的整个生命周期,一个很通用的概念就是Amazon’s的“you build, you run it”,它要求开发团队对软件产品的整个生命周期负责,这使得开发人员可以每天都关注产品的运行情况,而且也能够与用户保持紧密的联系,做一些必要的支持工作。

产品方式开发意味着与业务能力紧紧捆绑在一起,而不是将软件看成是一系列完成的功能,他们会关注如何让软件帮助其用户提升业务能力。
 
单体应用也可以采用上述产品的理念,但是更小粒度的服务可以更容易的创建开发者与用户之间的关系。

智能终端与弱管道

当在不同的进程之间构建各种通信结构时,我们已经看到许多产品和方法,来强调将大量的智能特性融入到通信机制本身,这种情况的一个典型例子就是“企业服务总线”(Enterprise Service Bus,ESB)。ESB产品经常包括高度智能的设施来进行消息的路由、编排、转换,并应用业务规则。

微服务社区主张采用另一种做法:智能终端和弱管道。使用微服务所构建的各个应用的目标都是尽可能实现“高内聚和低耦合”–他们拥有自己的领域逻辑,并且更多的是经典的UNIX的“过滤器”那样工作–即接收请求、处理逻辑、返回响应,这些应用通过使用简单的REST风格的协议来进行编排,而不去使用复杂的协议,比如:WS、BEPL或者集中式工具进行编排。

有两种协议最经常被使用到:包含资源API的HTTP的请求-响应和轻量级消息通信协议。最为重要的建议为:

微服务团队采用这样的原则和规范:基于互联网(广义上,包含Unix系统)构建系统。这样经常使用的资源几乎不用什么的代价就可以被开发者或者运行商缓存。

第二种做法是通过轻量级消息总线来发布消息。这种的通信协议非常的单一(单一到只负责消息路由),像RabbitMQ或者ZeroMQ这样的简单的实现甚至像可靠的异步机制都没提供,以至于需要依赖产生或者消费消息的终端或者服务来处理这类问题。
 
在一个单块系统中,各个组件在同一个进程中运行。它们相互之间的通信,要么通过方法调用,要么通过函数调用来进行。将一个单块系统改造为若干微服务的最大问题,在于对通信模式的改变。仅仅将内存中的方法调用转换为RPC调用这样天真的做法,会导致微服务之间产生繁琐的通信,使得系统表现变糟。取而代之的是,需要用更粗粒度的协议来替代细粒度的服务间通信。

小程聊微服务之性能测试场景设计杂谈

flychao88 发表了文章 • 7 个评论 • 411 次浏览 • 2017-06-15 15:32 • 来自相关话题

推荐人:程超

作者:张允庆,现就职于易宝支付有限公司,任职高级性能测试工程师,有多年的系统性能测试设计与优化经验,经历过大小上百个项目的性能优化,对性能测试有着较为深入的研究。2008年底获得北京大学理学学士学位,目前进入对外经济贸易大学在职研究生班进行深造,专业方向是大数据分析及应用。对性能测试相关话题感兴趣的读者可以和作者进行交流,电子邮箱地址:zhysync@163.com。

一、说在前面

提到性能测试,大家想到的就是使用工具对应用进行加压,看看应用能承受多少并发,TPS(Transactions Per Second)是多少,交易响应时间是否在接收的范围内。不错,这些都是大家最关心的应用的性能指标,也是每个性能测试项目输出的结果。然而,要实现这样的效果却并不是一件简单的事情,因为性能测试是一个十分复杂的系统工程,对测试人员的能力水平提出了更高的要求,需要性能测试人员具备非常全面的知识与技能,能够定位应用的性能瓶颈,并提出适当的优化方案。

通常,要对一个应用进行性能测试需要经历需求调研、环境准备、脚本开发、数据预埋、场景设计、场景执行、应用监控分析、瓶颈定位、瓶颈修复、回归测试、结果整理、输出报告等多个环节。

今天我们先谈一谈性能测试中的场景设计。
 
二、性能场景的定义

性能测试的场景如何定义?我们可以理解为功能测试中的用例,即性能测试的场景就是性能测试的用例。性能测试的场景是为了要实现特定的测试目标而对应用执行的压测活动。性能测试场景的设计与执行是整个性能测试活动的核心与灵魂,没有完整的场景设计就无法达到我们的测试目的,没有合理的场景设计就不会发现系统的性能缺陷。我们所开发的测试脚本,所预埋的测试数据都是为了实现特定场景所准备的。

一个性能测试场景包含诸多要素,图1中列出了一些必备的要素,其中测试模型作为测试场景的基础与输入。
 

 
下面对每一个要素做一个简单的说明:

1、测试模型与测试指标

在进行场景设计之前我们应该先确定了本次性能测试的测试指标与测试模型。测试指标和测试模型是进行场景设计的前提和基础,是场景的输入。根据被测系统的类型不同,可能测试指标的类型略有不同。对于在线web类的应用,测试指标一般包括在线用户数、最优并发用户数、最大并发用户数、交易平均响应时间、目标TPS等等。对于接口调用类的应用测试指标一般包括目标TPS、平均响应时间等。

测试模型就是被测试系统的各交易在线运行时承受的交易数量(或请求数量)的比例而不是并发用户的比例。为什么不是并发用户的比例呢?因为实际的用户的操作具有不确定性,使用测试工具很难模拟真实用户的行为。另外,在进行运营数据分析时很难获取用户的操作行为,而应用的交易记录却很容易通过查询的方式获取。应用实际承受的压力是用户的实际操作请求,在线用户如果没有进行实际操作那么他最多将消耗一个连接线程,而应用CPU并不会有什么资源消耗。100个用户平均每个花费10秒下一个订单和10个用户每1秒钟下一个订单对应用带来的压力是一样的。所以,在场景中能用最少的并发用户来模拟真实的请求是最经济的选择方式。

那么,测试模型到底该如何确定呢?通过需求调研获得。下面介绍的两点是我们常用的调研方式:
 
对于还未上线运营的新系统,我们一般会让应用的产品经理或负责人给出一个预估的比例;但是这个预估需要我们进行评估,不是随意的。对于一个以提供下单交易为主的应用,通常下单交易是占整个模型的较大比例,如果需求方提出的模型是查询比例较高,那么我们就有理由怀疑该模型的合理性。对于这种情况,我们建议选择几个常见的典型的模型来配合需求模型进行场景设计。

对于已上线运营的应用,我们一般会分析实际的交易数据来确定交易比例,这样会更加精准。例如一个应用对用户提供下单、查询、退款三个交易,我们通过DBA在线查询某日的交易数据总量为200000笔,其中交易下单160000笔、查询38000笔,退款2000笔,由此我们算出各交易的比例是80%、19%、1%,那么这个比例就是我们的测试模型。

2、被测交易或使用的脚本

测试脚本是测试场景的基础,脚本包括对应的测试数据,例如登录所需要的用户名与密码、下单交易可能需要的银行卡号等等。考虑到性能测试是多用户并发的测试,所以需要提前准备相应的测试数据,例如一个场景要对一个含登录操作的交易进行压测,那么我们在场景设计时就要考虑可用的用户名与密码数量;又如,要对退款交易做测试,那么就需要提前准备好可以退款的数据,这就需要提前做好数据预埋准备。
 
一般情况下,为了方便我们统计TPS,建议一个脚本只包含一个完整的交易,不要把多个交易放到一个脚本中。因为,不同的交易其响应时间会不同,响应时间较长的交易会成为“瓶颈”。另外,我们设计测试场景时需要考虑不同交易的占比,如果多个交易存在同一个脚本里,场景的设计就无法实现。

上文提到的“被测交易”是我们压测的对象,也是应用的入口。当然,并不是被测应用的每个交易都需要进行压测,这要视具体情况而定。如果被测应用提供的交易非常多,我们可以考虑只选取占比比较大的交易进行压测,而占比较低的交易可以忽略。

4、加压策略

加压策略就是并发用户以什么样的“步调”开始对应用发起请求。常用的并发策略有同时加载、指定间隔时间的加载,梯度加载等方式。加压策略的不同主要是模拟生产环境不同的情况,下面分别做简单介绍。

同时加载方式是指所有并发用户在场景启动时同时发起交易请求而不包含任何等待,这样会对被测应用带来突然的压力,用于考察应用在突然加压下的表现是否符合预期。一般有用户突增的业务特点的应用会设计这样的场景,例如,某些抢购系统、铁路售票系统的按时放票功能等。当然,对于那些并发用户较少的场景也可以采用这种用户加载方式。而对于有些应用如果同时加载大量的并发用户可能会出现异常或超时,导致部分并发用户失败。
 
指定间隔时间的加载方式是我们最常用的,这是为了模拟生产的实际情况,一般生产系统接收用户请求都是逐渐增加的,到当日交易的高峰时段达到最大。在场景设计时,根据并发用户的多少可以设置适当的增加频率,一般是“多长时间增加多少用户”。例如,每一秒钟增加一个用户、每两秒增加5个用户等等。 
梯度加压策略也是我们常用的一种用户加载方式,但是这种方式严格来说应该是一种梯度加压场景。该场景一般是预先设置几个并发用户的梯度,每个梯度执行几分钟,这样就可以通过一个场景的执行基本上找到应用的最大TPS。在下文场景类型中,我们会详细介绍这种场景。

5、运行时间

每个类型的场景其执行时间是不同的。表1为大家提供一个参考值。运行时间是不包含用户的加载时间和退出时间的,即全部用户都在执行的这段时间。
 

6、延时方式

延时是上一笔请求完成到下一笔请求发起之间的时间间隔。延时在场景中的作用就是为了精准控制TPS,或者降低当前并发用户数量下的压力。而精准控制TPS的目的就是考察应用在特定压力下是否存在性能问题。在性能测试工具LoadRunner中提供了三种延时设置方式:

第一种是上一次请求完成后立即发起下一次请求,也就是延时为0。

第二种是上一次请求完成后间隔指定的时间后再发起下次请求。

第三种是在指定时间内完成一次请求,即区间型的延时,这要求我们设置的这个时间要大于交易的响应时间,也就是说要保证交易响应时间在我设置的这个时间的区间内,否则就不能实现精准控制TPS的目标。在这个区间内,交易响应时间无论如何变化,只要不突破我的这个最大区间,那么TPS就是平稳的。

在实际的场景设置中,为了实现精准的TPS控制目标,我们选用第三种设置方式。通过不断地尝试与调整,最终能够达到目标TPS。
 
7、用户终止方式

和用户加载方式对应,用户终止方式是场景执行完成后的用户退出方式。一般使用的是“同时退出”和“每隔多少时间退出几个用户”这种方式。这里我们重点介绍一下“同时退出”这种方式。应用在持续一段时间的压力后如果突然压力全部释放了,那么这时的应用在理想情况下应该是怎样的?CPU资源应该从繁忙立即变为空闲,网络传输也大幅降低,磁盘IO降为0等等。不然,那就是有某种问题的存在了。这时候就需要分析导致资源不能释放的原因。

8、各种资源的监控方式

资源的监控方式也是我们场景设计时必须考虑的一个必要因素,在场景设计时就应该确定每个场景的资源监控策略。这些策略包括监控的对象、使用的监控工具或方法、监控数据采集频率等。监控对象一般是测试环境中所有操作系统资源使用(CPU、内存、磁盘IO、网络吞吐等)、数据库(TOP SQL、数据库锁等待与死锁、缓冲区命中率等)、JVM的运行情况(堆内存垃圾回收、线程状态、数据库连接池使用情况等)等。监控数据采集频率也会因场景执行的时间长度不同而进行适当调整,例如混合容量场景如果执行30分钟,那么采集频率可以为每5秒钟采集一次,共采集360次。但是,考虑到监控要提前启动,所以采集次数可以适当增加一些,这样可以确保整个监控区间大于场景执行区间,也就同时监控到了资源使用在压力发起前后的变化情况。对于执行时间较长的场景,我们就要适当调整采集间隔和采集次数,例如对于一个执行12小时的稳定性场景,我们可以每50秒采集一次,共采集1000次。
 
三、常见的场景类型

1、单交易基准

一般使用一个用户或一个线程,延时设置为0,对一个交易持续运行10分钟以上。该场景的主要目的是获取单个交易在无压力的情况下的基准响应时间及环境资源使用情况,作为其他场景的参考依据。

2、单交易负载

单交易负载的场景是为了找到单个交易的最优TPS,检测单交易在并发情况下是否存在性能瓶颈。这个最优是以什么为衡量标准呢?通常以应用或数据库等系统的CPU使用率不大于70%为标准。为什么是70%?不能更高了吗?通常在生产上运行的应用,如果CPU使用率长期处于高水平那是非常严重的问题,应用的节点随时都可能挂掉。对于生产环境各种资源的使用情况,通常运维部门都会有实时的监控,一般当摸个节点的CPU使用率超过50%时就会触发报警,如果长时间处于高负载状态,那么说明应用节点可用资源不足,就应该考虑进行节点扩充了。

当然,也并不是什么情况下都需要找到单交易的最优TPS,这要分情况来对待。对于被测应用提供的交易比较少的话,可以通过不断测试找到每个交易的最优TPS。但是,有的应用提供的交易比较多,这时如果每个交易的最优TPS都要找到,那就会需要较多的时间来进行测试。

单交易负载的场景具体该如何设计与执行呢?如果你想找出每个交易的最优TPS,可以从5个并发用户开始,执行几分钟后再增加5个用户,直到应用CPU使用率超过70%为止。场景的延时设置为0,场景执行前需开启相关监控。该场景用于获取单交易在并发情况下的响应时间与TPS,发现交易本身是否存在并发问题,应用是否会出现错误和异常,响应时间相对单交易基准是否有明显的提高,资源使用率是否在合理的承受范围之内等等。如果应用存在性能缺陷该场景即可发现。
 
当然,如果你不想测试出每个交易的最优TPS,那么单独对每个交易做一次5个并发的负载测试即可。

3、多交易混合负载

多交易混合负载的目的是为了找到应用的最优TPS,即应用CPU资源消耗在70%左右时的TPS(此时需确保数据库等其他被调用资源不成为瓶颈)。

按照测试模型中的交易比例及目标TPS,对每个交易分配不同的并发用户数量,设置不同的延时,同时进行加压,通过多个子场景的不断尝试最终测试出应用能够达到的最优TPS。这个场景比较复杂,一般需要经过多次的测试与调整才能到达测试模型的比例要求。经过单交易负载测试之后我们已经获取了每个交易的平均响应时间,那么由此值我们便可以设置我们的混合负载场景。假设,我们应用的测试指标TPS为100,单交易负载测试获取的各交易响应时间如下:下单0.4秒,查询0.2秒,退款0.5秒,那么要达到100TPS的压力,该如何设置场景?

计算每个交易的TPS 
下单TPS=100*80%=80,查询TPS=100*19%=19,退款TPS=100*1%=1

确定每个交易的并发用户 
目标TPS、响应时间、并发用户之间有这样一个关系: 
TPS=并发用户/响应时间
 
如果一个交易响应时间是0.2秒,那一个用户时的TPS就是1/0.2=5。

在咱们这个实例中每个交易的并发用户计算如下: 
下单交易并发用户数量=80*0.4=32 
查询交易并发用户数量=19*0.2=3.8 
退款交易并发用户数量=1*0.5=0.5

大家看到了,这里出现了非整数的情况,怎么办?对于这种情况我们要进行整数化处理。即我们一般取大于并最接近当前数的整数,3.8我们按4,0.5我们按1。整数化后对应的响应时间也应该发生变化,否则就无法实现目标TPS。整数化再次计算实际的响应时间: 
查询交易调整后的响应时间=4/19=0.21 
退款交易调整后的响应时间=1/1=1

于是场景设置如下,下单交易并发用户32个延时设置为0秒,查询交易并发用户4个延时设置0.01秒,退款交易并发用户1个延时设置0.5秒,场景运行时间10分钟以上。但是这个场景运行结果可能并不会完全符合我们的预期,因为并发用户相比单交易负载场景已经增加了很多,交易的响应时间很可能会出现明显的延长。比如下单交易的实际响应时间可能会延长到0.6秒,那么实际的TPS将明显下降。如果出现这种情况该如何处理呢?我们推荐使用LoadRunner的区间型延时设置,将这个“区间时间”设置的比实际交易响应时间大一些,根据这个时间再计算对应的并发用户量。另外,建议大家建一个excel的表格,用于计算延时和并发用户的值,效果见下表。
 

表2中的列“PACING”的字段的值是使用公式自动计算出来的,公式为“=USERS单元格编号/(目标TPS单元格编号*交易占比单元格编号)”。建立这个表之后我们只需手动修改两个列的值就可以方便地计算出每个场景下的每个交易的延时,这两个列就是“平均响应时间”和“USERS”。平均响应时间随着并发用户的增加必然会相应地增长,所以在表2中每个场景的平均响应时间数据都是上个场景的执行结果。这样我们每执行完成一个场景,然后就把响应时间的数据填写到下个场景中,然后再修改并发用户数量,并确保PACING 大于平均响应时间即可,如果在测试执行过程中出现平均响应时间大于PACING时需要停止场景重新计算。 
综上所述,多交易混合负载场景并不是一个场景,而是一系列混合场景的集合。还以上例来说,目标100TPS时我们分析监控结果发现系统各项资源利用率都不是太高,这说明应用还能够承受更大的压力。这就需要我们继续加大压力进行测试。我们可能的场景是150TPS、200TPS等等。那么如何确定我们的压力梯度呢?这就要看系统资源到底使用了多少,如果100TPS时发现系统各项资源使用率在50%左右,我们就可以估计应用的最优TPS应该能够达到150,那么我们下一个场景就是要按150TPS的目标压力去发压,相关的并发用户和延时根据上表进行调整即可。如果不能实现150TPS的压力,那么我们就要减少目标TPS再进行发压,直到测试获取到应用的最优TPS。

4、多交易混合容量

容量的意思就是应用能够达到的最大TPS。该场景是和多交易混合负载场景相关联的,即通过多交易混合负载找出应用承受的最优TPS后继续对应用进行加压,直到找到应用的最大TPS。混合容量场景的并发用户与延时调节方式和混合负载一样,在这里就不再赘述了。
 
5、大并发

该类场景的目的是考察系统在大并发的情况下是否存在问题,是否有报错,是否有用户失败等。大并发一般要设置一个延时,用于到达最优并发时的TPS。那么,大并发时的用户到底设置多少,这个延时要设置多久,依据是什么呢?一般我们设置的并发用户数量是最优并发的5至10倍,而延时要通过计算得到。这里还是举例说明,有一个应用,测试得到的最大TPS为200,对应的并发用户为20,那么我们可以设置两个大并发场景,即100并发用户和200并发用户。100并发时的延时设置为100/200TPS=0.5秒,200并发时的延时设置为200/200TPS=1秒,这个延时为区间型的延时。

通常,在进行大并发测试时获得的TPS结果要比最大TPS低很多,因为在大并发时系统很有可能出现某些资源不够用,线程很可能会出现严重的阻塞等等。 
如何考量大并发测试获得的测试结果是否符合预期,或者说大并发测试通过的标准是什么?这个也没有固定的标准可循,通常我们认为只要符合如下两方面的要求即可认为测试通过。

最大并发用户量是否能达到最大TPS时的5倍;

测试结果的TPS是否达到测试指标的要求;
 
需要说明的是这里的大并发和应用的最优并发与最大并发并不是一回事,二者并不相同。

6、稳定性

给应用一个恒定的压力,使场景运行较长的时间,用于测试应用在长时间运行下的表现,TPS是否有较大波动、是否有错误和异常、是否存在内存溢出等。根据业务类型不同一般会运行不同的时长,对于5*8这样的应用稳定性运行8小时即可,7*24这样的应用最好能够运行12小时以上。

恒定的压力怎么选取?通常有两种方式。第一种,选择应用最优压力的80%最为目标压力,这种方式比较适合应用的最优TPS不是很高的应用,比如200以下;第二种,如果应用的TPS比较高,那么我们需要换一种方式,否则就会产生较多的测试数据。例如一个应用的最优TPS为1000笔/秒,如果我们取其80%的压力800笔/秒,那么加压12小时的数据量为3456万!这时,我们使用200TPS的恒定压力运行12小时即可。

7、扩展性

考察应用的扩展能力。未扩展的情况下基本是一个子系统使用一个单独的机器节点,也就是应用的单点情况。扩展性就是,再对应用进行一个节点的扩展,测试扩展情况下的TPS。一般双节点的总TPS达到单节点的1.8倍即认为系统具有良好的扩展性。压测时我们选取混合容量场景中获取到应用最大TPS时的场景做为压测场景,并使用不同的压力机分别对两个节点进行加压,考察测试结果能够达到多少TPS。

8、可靠性或异常测试

这种情况下一般是将压力做为背景,对应用所依赖的环境进行模拟故障,考察应用的表现是否符合预期。例如,在一定压力背景下,模拟网络的闪断,待网络恢复后应用TPS是否能够及时恢复。背景压力我们一般选取混合负载测试获取最优TPS时的场景即可。
 
9、影响性

影响性测试也是性能测试过程中经常遇到一类场景。这种场景一般是针对提供非实时功能的应用所设计的。例如,有批处理或异步处理的应用。严格来说,这不应该算是一个单独场景类型,应该是一种特定的测试类型。对于这类的测试我们一般分两步来执行,首先是在未启用具有影响性的功能时测试出应用的最优和最大TPS;其次,启动具有影响性的功能,再按第一步的场景(场景的设置均不变)进行测试,对比二者的TPS差别,这个差别就是我们要考察的影响性。

10、挡板延时对比

如果压测环境使用了挡板,可以通过挡板来设置不同的延时进行对比测试。比如延时设置为0.5秒,1秒,甚至2秒。根据不同的延时设置,增加相应的并发用户数量,调整场景的各项设置,考察应用是否能够达到最大TPS,是否出现并发用户失败或应用异常等。对于挡板延时,一般的作用是模拟被调用的系统的延迟,考察被测应用在不同的延时情况下的性能表现。现在的应用系统很少有是完全独立的了,或多或少地都需要调用别的系统来实现某些操作或业务。例如,对于一个支付系统,起码需要调用银行通道、银联通道、手机短信通道等等第三方系统。由于受网络传输、被调系统的性能等多方面的影响,每次调用外围系统都会消耗一定的时间,综合起来我们一般会计算一个平均值,那么这个值就是被调用系统的平均延时。

11、在线用户

web类的应用一般会有在线用户这样一个测试场景。这个场景主要的考察目标是在大量用户在线的情况下,系统是否会出现异常。在线用户即登录系统后并未执行任何操作的用户或执行操作后未退出的用户。脚本里设置一个足够长的思考时间,让用户反复执行“登录-在线-退出”这样的过程。一般这种场景模拟的用户数量较大。
 
12、最优并发用户

这个场景是为了找到应用的最优并发用户数量。最优并发用户数量是指应用达到最大TPS时的并发用户数量,这一点和最优TPS的定义是有区别的。通常在进行多交易混合容量场景执行过程中测试出应用最大TPS时的并发用户数量即为最优并发用户数量,故此场景可以和多交易混合容量场景合并执行。

13、最大并发用户

这个场景是为了找到应用能够承受的最大并发用户数量。最大并发用户数量是应用能够承受的并发用户的极限,这时要求用户不会出现失败,交易响应时间不能超过指标的要求,应用不会出现异常、错误等非正常现象。在测试过程中,当我们测试到最大TPS后继续增加并发用户数量,直到出现应用异常,或出现并发用户失败,或交易响应时间超过测试指标要求等,这时的并发用户数量即为最大并发用户。

对于最优并发用户或最大并发用户的场景,一般测试web类的应用或有明确并发用户指标需求的应用时会设计这样的测试场景,而接口类的应用一般不考虑这两个场景。

14、梯度加压

这种类型场景一般是为了“偷懒”而设计的。比如,在生产环境要测试一个交易的最大TPS能够到多少时,我们为了节省宝贵的测试时间,一般会使用梯度加压的场景策略。这时我们不知道被测环境能够达到什么样的吞吐量,也没有明确的测试指标,为了快速找到应用的最大TPS,使用梯度场景是最简单有效的。
 
所谓梯度是指开始使用较少的用户加压一段时间(几分钟即可),待TPS稳定后再继续往上加用户,如此循环,直到TPS不再增加为止。整个过程就像爬楼梯一样,所以称为“梯度”。图2是某个交易的梯度加压场景的图示,横轴是场景运行时间,纵轴是并发用户数,图中场景的策略是每个梯度加载5个用户并执行5分钟,整个场景最大并发为20,执行时间为20分钟。
  查看全部
推荐人:程超

作者:张允庆,现就职于易宝支付有限公司,任职高级性能测试工程师,有多年的系统性能测试设计与优化经验,经历过大小上百个项目的性能优化,对性能测试有着较为深入的研究。2008年底获得北京大学理学学士学位,目前进入对外经济贸易大学在职研究生班进行深造,专业方向是大数据分析及应用。对性能测试相关话题感兴趣的读者可以和作者进行交流,电子邮箱地址:zhysync@163.com

一、说在前面

提到性能测试,大家想到的就是使用工具对应用进行加压,看看应用能承受多少并发,TPS(Transactions Per Second)是多少,交易响应时间是否在接收的范围内。不错,这些都是大家最关心的应用的性能指标,也是每个性能测试项目输出的结果。然而,要实现这样的效果却并不是一件简单的事情,因为性能测试是一个十分复杂的系统工程,对测试人员的能力水平提出了更高的要求,需要性能测试人员具备非常全面的知识与技能,能够定位应用的性能瓶颈,并提出适当的优化方案。

通常,要对一个应用进行性能测试需要经历需求调研、环境准备、脚本开发、数据预埋、场景设计、场景执行、应用监控分析、瓶颈定位、瓶颈修复、回归测试、结果整理、输出报告等多个环节。

今天我们先谈一谈性能测试中的场景设计。
 
二、性能场景的定义

性能测试的场景如何定义?我们可以理解为功能测试中的用例,即性能测试的场景就是性能测试的用例。性能测试的场景是为了要实现特定的测试目标而对应用执行的压测活动。性能测试场景的设计与执行是整个性能测试活动的核心与灵魂,没有完整的场景设计就无法达到我们的测试目的,没有合理的场景设计就不会发现系统的性能缺陷。我们所开发的测试脚本,所预埋的测试数据都是为了实现特定场景所准备的。

一个性能测试场景包含诸多要素,图1中列出了一些必备的要素,其中测试模型作为测试场景的基础与输入。
 

 
下面对每一个要素做一个简单的说明:

1、测试模型与测试指标

在进行场景设计之前我们应该先确定了本次性能测试的测试指标与测试模型。测试指标和测试模型是进行场景设计的前提和基础,是场景的输入。根据被测系统的类型不同,可能测试指标的类型略有不同。对于在线web类的应用,测试指标一般包括在线用户数、最优并发用户数、最大并发用户数、交易平均响应时间、目标TPS等等。对于接口调用类的应用测试指标一般包括目标TPS、平均响应时间等。

测试模型就是被测试系统的各交易在线运行时承受的交易数量(或请求数量)的比例而不是并发用户的比例。为什么不是并发用户的比例呢?因为实际的用户的操作具有不确定性,使用测试工具很难模拟真实用户的行为。另外,在进行运营数据分析时很难获取用户的操作行为,而应用的交易记录却很容易通过查询的方式获取。应用实际承受的压力是用户的实际操作请求,在线用户如果没有进行实际操作那么他最多将消耗一个连接线程,而应用CPU并不会有什么资源消耗。100个用户平均每个花费10秒下一个订单和10个用户每1秒钟下一个订单对应用带来的压力是一样的。所以,在场景中能用最少的并发用户来模拟真实的请求是最经济的选择方式。

那么,测试模型到底该如何确定呢?通过需求调研获得。下面介绍的两点是我们常用的调研方式:
 
对于还未上线运营的新系统,我们一般会让应用的产品经理或负责人给出一个预估的比例;但是这个预估需要我们进行评估,不是随意的。对于一个以提供下单交易为主的应用,通常下单交易是占整个模型的较大比例,如果需求方提出的模型是查询比例较高,那么我们就有理由怀疑该模型的合理性。对于这种情况,我们建议选择几个常见的典型的模型来配合需求模型进行场景设计。

对于已上线运营的应用,我们一般会分析实际的交易数据来确定交易比例,这样会更加精准。例如一个应用对用户提供下单、查询、退款三个交易,我们通过DBA在线查询某日的交易数据总量为200000笔,其中交易下单160000笔、查询38000笔,退款2000笔,由此我们算出各交易的比例是80%、19%、1%,那么这个比例就是我们的测试模型。

2、被测交易或使用的脚本

测试脚本是测试场景的基础,脚本包括对应的测试数据,例如登录所需要的用户名与密码、下单交易可能需要的银行卡号等等。考虑到性能测试是多用户并发的测试,所以需要提前准备相应的测试数据,例如一个场景要对一个含登录操作的交易进行压测,那么我们在场景设计时就要考虑可用的用户名与密码数量;又如,要对退款交易做测试,那么就需要提前准备好可以退款的数据,这就需要提前做好数据预埋准备。
 
一般情况下,为了方便我们统计TPS,建议一个脚本只包含一个完整的交易,不要把多个交易放到一个脚本中。因为,不同的交易其响应时间会不同,响应时间较长的交易会成为“瓶颈”。另外,我们设计测试场景时需要考虑不同交易的占比,如果多个交易存在同一个脚本里,场景的设计就无法实现。

上文提到的“被测交易”是我们压测的对象,也是应用的入口。当然,并不是被测应用的每个交易都需要进行压测,这要视具体情况而定。如果被测应用提供的交易非常多,我们可以考虑只选取占比比较大的交易进行压测,而占比较低的交易可以忽略。

4、加压策略

加压策略就是并发用户以什么样的“步调”开始对应用发起请求。常用的并发策略有同时加载、指定间隔时间的加载,梯度加载等方式。加压策略的不同主要是模拟生产环境不同的情况,下面分别做简单介绍。

同时加载方式是指所有并发用户在场景启动时同时发起交易请求而不包含任何等待,这样会对被测应用带来突然的压力,用于考察应用在突然加压下的表现是否符合预期。一般有用户突增的业务特点的应用会设计这样的场景,例如,某些抢购系统、铁路售票系统的按时放票功能等。当然,对于那些并发用户较少的场景也可以采用这种用户加载方式。而对于有些应用如果同时加载大量的并发用户可能会出现异常或超时,导致部分并发用户失败。
 
指定间隔时间的加载方式是我们最常用的,这是为了模拟生产的实际情况,一般生产系统接收用户请求都是逐渐增加的,到当日交易的高峰时段达到最大。在场景设计时,根据并发用户的多少可以设置适当的增加频率,一般是“多长时间增加多少用户”。例如,每一秒钟增加一个用户、每两秒增加5个用户等等。 
梯度加压策略也是我们常用的一种用户加载方式,但是这种方式严格来说应该是一种梯度加压场景。该场景一般是预先设置几个并发用户的梯度,每个梯度执行几分钟,这样就可以通过一个场景的执行基本上找到应用的最大TPS。在下文场景类型中,我们会详细介绍这种场景。

5、运行时间

每个类型的场景其执行时间是不同的。表1为大家提供一个参考值。运行时间是不包含用户的加载时间和退出时间的,即全部用户都在执行的这段时间。
 

6、延时方式

延时是上一笔请求完成到下一笔请求发起之间的时间间隔。延时在场景中的作用就是为了精准控制TPS,或者降低当前并发用户数量下的压力。而精准控制TPS的目的就是考察应用在特定压力下是否存在性能问题。在性能测试工具LoadRunner中提供了三种延时设置方式:

第一种是上一次请求完成后立即发起下一次请求,也就是延时为0。

第二种是上一次请求完成后间隔指定的时间后再发起下次请求。

第三种是在指定时间内完成一次请求,即区间型的延时,这要求我们设置的这个时间要大于交易的响应时间,也就是说要保证交易响应时间在我设置的这个时间的区间内,否则就不能实现精准控制TPS的目标。在这个区间内,交易响应时间无论如何变化,只要不突破我的这个最大区间,那么TPS就是平稳的。

在实际的场景设置中,为了实现精准的TPS控制目标,我们选用第三种设置方式。通过不断地尝试与调整,最终能够达到目标TPS。
 
7、用户终止方式

和用户加载方式对应,用户终止方式是场景执行完成后的用户退出方式。一般使用的是“同时退出”和“每隔多少时间退出几个用户”这种方式。这里我们重点介绍一下“同时退出”这种方式。应用在持续一段时间的压力后如果突然压力全部释放了,那么这时的应用在理想情况下应该是怎样的?CPU资源应该从繁忙立即变为空闲,网络传输也大幅降低,磁盘IO降为0等等。不然,那就是有某种问题的存在了。这时候就需要分析导致资源不能释放的原因。

8、各种资源的监控方式

资源的监控方式也是我们场景设计时必须考虑的一个必要因素,在场景设计时就应该确定每个场景的资源监控策略。这些策略包括监控的对象、使用的监控工具或方法、监控数据采集频率等。监控对象一般是测试环境中所有操作系统资源使用(CPU、内存、磁盘IO、网络吞吐等)、数据库(TOP SQL、数据库锁等待与死锁、缓冲区命中率等)、JVM的运行情况(堆内存垃圾回收、线程状态、数据库连接池使用情况等)等。监控数据采集频率也会因场景执行的时间长度不同而进行适当调整,例如混合容量场景如果执行30分钟,那么采集频率可以为每5秒钟采集一次,共采集360次。但是,考虑到监控要提前启动,所以采集次数可以适当增加一些,这样可以确保整个监控区间大于场景执行区间,也就同时监控到了资源使用在压力发起前后的变化情况。对于执行时间较长的场景,我们就要适当调整采集间隔和采集次数,例如对于一个执行12小时的稳定性场景,我们可以每50秒采集一次,共采集1000次。
 
三、常见的场景类型

1、单交易基准

一般使用一个用户或一个线程,延时设置为0,对一个交易持续运行10分钟以上。该场景的主要目的是获取单个交易在无压力的情况下的基准响应时间及环境资源使用情况,作为其他场景的参考依据。

2、单交易负载

单交易负载的场景是为了找到单个交易的最优TPS,检测单交易在并发情况下是否存在性能瓶颈。这个最优是以什么为衡量标准呢?通常以应用或数据库等系统的CPU使用率不大于70%为标准。为什么是70%?不能更高了吗?通常在生产上运行的应用,如果CPU使用率长期处于高水平那是非常严重的问题,应用的节点随时都可能挂掉。对于生产环境各种资源的使用情况,通常运维部门都会有实时的监控,一般当摸个节点的CPU使用率超过50%时就会触发报警,如果长时间处于高负载状态,那么说明应用节点可用资源不足,就应该考虑进行节点扩充了。

当然,也并不是什么情况下都需要找到单交易的最优TPS,这要分情况来对待。对于被测应用提供的交易比较少的话,可以通过不断测试找到每个交易的最优TPS。但是,有的应用提供的交易比较多,这时如果每个交易的最优TPS都要找到,那就会需要较多的时间来进行测试。

单交易负载的场景具体该如何设计与执行呢?如果你想找出每个交易的最优TPS,可以从5个并发用户开始,执行几分钟后再增加5个用户,直到应用CPU使用率超过70%为止。场景的延时设置为0,场景执行前需开启相关监控。该场景用于获取单交易在并发情况下的响应时间与TPS,发现交易本身是否存在并发问题,应用是否会出现错误和异常,响应时间相对单交易基准是否有明显的提高,资源使用率是否在合理的承受范围之内等等。如果应用存在性能缺陷该场景即可发现。
 
当然,如果你不想测试出每个交易的最优TPS,那么单独对每个交易做一次5个并发的负载测试即可。

3、多交易混合负载

多交易混合负载的目的是为了找到应用的最优TPS,即应用CPU资源消耗在70%左右时的TPS(此时需确保数据库等其他被调用资源不成为瓶颈)。

按照测试模型中的交易比例及目标TPS,对每个交易分配不同的并发用户数量,设置不同的延时,同时进行加压,通过多个子场景的不断尝试最终测试出应用能够达到的最优TPS。这个场景比较复杂,一般需要经过多次的测试与调整才能到达测试模型的比例要求。经过单交易负载测试之后我们已经获取了每个交易的平均响应时间,那么由此值我们便可以设置我们的混合负载场景。假设,我们应用的测试指标TPS为100,单交易负载测试获取的各交易响应时间如下:下单0.4秒,查询0.2秒,退款0.5秒,那么要达到100TPS的压力,该如何设置场景?

计算每个交易的TPS 
下单TPS=100*80%=80,查询TPS=100*19%=19,退款TPS=100*1%=1

确定每个交易的并发用户 
目标TPS、响应时间、并发用户之间有这样一个关系: 
TPS=并发用户/响应时间
 
如果一个交易响应时间是0.2秒,那一个用户时的TPS就是1/0.2=5。

在咱们这个实例中每个交易的并发用户计算如下: 
下单交易并发用户数量=80*0.4=32 
查询交易并发用户数量=19*0.2=3.8 
退款交易并发用户数量=1*0.5=0.5

大家看到了,这里出现了非整数的情况,怎么办?对于这种情况我们要进行整数化处理。即我们一般取大于并最接近当前数的整数,3.8我们按4,0.5我们按1。整数化后对应的响应时间也应该发生变化,否则就无法实现目标TPS。整数化再次计算实际的响应时间: 
查询交易调整后的响应时间=4/19=0.21 
退款交易调整后的响应时间=1/1=1

于是场景设置如下,下单交易并发用户32个延时设置为0秒,查询交易并发用户4个延时设置0.01秒,退款交易并发用户1个延时设置0.5秒,场景运行时间10分钟以上。但是这个场景运行结果可能并不会完全符合我们的预期,因为并发用户相比单交易负载场景已经增加了很多,交易的响应时间很可能会出现明显的延长。比如下单交易的实际响应时间可能会延长到0.6秒,那么实际的TPS将明显下降。如果出现这种情况该如何处理呢?我们推荐使用LoadRunner的区间型延时设置,将这个“区间时间”设置的比实际交易响应时间大一些,根据这个时间再计算对应的并发用户量。另外,建议大家建一个excel的表格,用于计算延时和并发用户的值,效果见下表。
 

表2中的列“PACING”的字段的值是使用公式自动计算出来的,公式为“=USERS单元格编号/(目标TPS单元格编号*交易占比单元格编号)”。建立这个表之后我们只需手动修改两个列的值就可以方便地计算出每个场景下的每个交易的延时,这两个列就是“平均响应时间”和“USERS”。平均响应时间随着并发用户的增加必然会相应地增长,所以在表2中每个场景的平均响应时间数据都是上个场景的执行结果。这样我们每执行完成一个场景,然后就把响应时间的数据填写到下个场景中,然后再修改并发用户数量,并确保PACING 大于平均响应时间即可,如果在测试执行过程中出现平均响应时间大于PACING时需要停止场景重新计算。 
综上所述,多交易混合负载场景并不是一个场景,而是一系列混合场景的集合。还以上例来说,目标100TPS时我们分析监控结果发现系统各项资源利用率都不是太高,这说明应用还能够承受更大的压力。这就需要我们继续加大压力进行测试。我们可能的场景是150TPS、200TPS等等。那么如何确定我们的压力梯度呢?这就要看系统资源到底使用了多少,如果100TPS时发现系统各项资源使用率在50%左右,我们就可以估计应用的最优TPS应该能够达到150,那么我们下一个场景就是要按150TPS的目标压力去发压,相关的并发用户和延时根据上表进行调整即可。如果不能实现150TPS的压力,那么我们就要减少目标TPS再进行发压,直到测试获取到应用的最优TPS。

4、多交易混合容量

容量的意思就是应用能够达到的最大TPS。该场景是和多交易混合负载场景相关联的,即通过多交易混合负载找出应用承受的最优TPS后继续对应用进行加压,直到找到应用的最大TPS。混合容量场景的并发用户与延时调节方式和混合负载一样,在这里就不再赘述了。
 
5、大并发

该类场景的目的是考察系统在大并发的情况下是否存在问题,是否有报错,是否有用户失败等。大并发一般要设置一个延时,用于到达最优并发时的TPS。那么,大并发时的用户到底设置多少,这个延时要设置多久,依据是什么呢?一般我们设置的并发用户数量是最优并发的5至10倍,而延时要通过计算得到。这里还是举例说明,有一个应用,测试得到的最大TPS为200,对应的并发用户为20,那么我们可以设置两个大并发场景,即100并发用户和200并发用户。100并发时的延时设置为100/200TPS=0.5秒,200并发时的延时设置为200/200TPS=1秒,这个延时为区间型的延时。

通常,在进行大并发测试时获得的TPS结果要比最大TPS低很多,因为在大并发时系统很有可能出现某些资源不够用,线程很可能会出现严重的阻塞等等。 
如何考量大并发测试获得的测试结果是否符合预期,或者说大并发测试通过的标准是什么?这个也没有固定的标准可循,通常我们认为只要符合如下两方面的要求即可认为测试通过。

最大并发用户量是否能达到最大TPS时的5倍;

测试结果的TPS是否达到测试指标的要求;
 
需要说明的是这里的大并发和应用的最优并发与最大并发并不是一回事,二者并不相同。

6、稳定性

给应用一个恒定的压力,使场景运行较长的时间,用于测试应用在长时间运行下的表现,TPS是否有较大波动、是否有错误和异常、是否存在内存溢出等。根据业务类型不同一般会运行不同的时长,对于5*8这样的应用稳定性运行8小时即可,7*24这样的应用最好能够运行12小时以上。

恒定的压力怎么选取?通常有两种方式。第一种,选择应用最优压力的80%最为目标压力,这种方式比较适合应用的最优TPS不是很高的应用,比如200以下;第二种,如果应用的TPS比较高,那么我们需要换一种方式,否则就会产生较多的测试数据。例如一个应用的最优TPS为1000笔/秒,如果我们取其80%的压力800笔/秒,那么加压12小时的数据量为3456万!这时,我们使用200TPS的恒定压力运行12小时即可。

7、扩展性

考察应用的扩展能力。未扩展的情况下基本是一个子系统使用一个单独的机器节点,也就是应用的单点情况。扩展性就是,再对应用进行一个节点的扩展,测试扩展情况下的TPS。一般双节点的总TPS达到单节点的1.8倍即认为系统具有良好的扩展性。压测时我们选取混合容量场景中获取到应用最大TPS时的场景做为压测场景,并使用不同的压力机分别对两个节点进行加压,考察测试结果能够达到多少TPS。

8、可靠性或异常测试

这种情况下一般是将压力做为背景,对应用所依赖的环境进行模拟故障,考察应用的表现是否符合预期。例如,在一定压力背景下,模拟网络的闪断,待网络恢复后应用TPS是否能够及时恢复。背景压力我们一般选取混合负载测试获取最优TPS时的场景即可。
 
9、影响性

影响性测试也是性能测试过程中经常遇到一类场景。这种场景一般是针对提供非实时功能的应用所设计的。例如,有批处理或异步处理的应用。严格来说,这不应该算是一个单独场景类型,应该是一种特定的测试类型。对于这类的测试我们一般分两步来执行,首先是在未启用具有影响性的功能时测试出应用的最优和最大TPS;其次,启动具有影响性的功能,再按第一步的场景(场景的设置均不变)进行测试,对比二者的TPS差别,这个差别就是我们要考察的影响性。

10、挡板延时对比

如果压测环境使用了挡板,可以通过挡板来设置不同的延时进行对比测试。比如延时设置为0.5秒,1秒,甚至2秒。根据不同的延时设置,增加相应的并发用户数量,调整场景的各项设置,考察应用是否能够达到最大TPS,是否出现并发用户失败或应用异常等。对于挡板延时,一般的作用是模拟被调用的系统的延迟,考察被测应用在不同的延时情况下的性能表现。现在的应用系统很少有是完全独立的了,或多或少地都需要调用别的系统来实现某些操作或业务。例如,对于一个支付系统,起码需要调用银行通道、银联通道、手机短信通道等等第三方系统。由于受网络传输、被调系统的性能等多方面的影响,每次调用外围系统都会消耗一定的时间,综合起来我们一般会计算一个平均值,那么这个值就是被调用系统的平均延时。

11、在线用户

web类的应用一般会有在线用户这样一个测试场景。这个场景主要的考察目标是在大量用户在线的情况下,系统是否会出现异常。在线用户即登录系统后并未执行任何操作的用户或执行操作后未退出的用户。脚本里设置一个足够长的思考时间,让用户反复执行“登录-在线-退出”这样的过程。一般这种场景模拟的用户数量较大。
 
12、最优并发用户

这个场景是为了找到应用的最优并发用户数量。最优并发用户数量是指应用达到最大TPS时的并发用户数量,这一点和最优TPS的定义是有区别的。通常在进行多交易混合容量场景执行过程中测试出应用最大TPS时的并发用户数量即为最优并发用户数量,故此场景可以和多交易混合容量场景合并执行。

13、最大并发用户

这个场景是为了找到应用能够承受的最大并发用户数量。最大并发用户数量是应用能够承受的并发用户的极限,这时要求用户不会出现失败,交易响应时间不能超过指标的要求,应用不会出现异常、错误等非正常现象。在测试过程中,当我们测试到最大TPS后继续增加并发用户数量,直到出现应用异常,或出现并发用户失败,或交易响应时间超过测试指标要求等,这时的并发用户数量即为最大并发用户。

对于最优并发用户或最大并发用户的场景,一般测试web类的应用或有明确并发用户指标需求的应用时会设计这样的测试场景,而接口类的应用一般不考虑这两个场景。

14、梯度加压

这种类型场景一般是为了“偷懒”而设计的。比如,在生产环境要测试一个交易的最大TPS能够到多少时,我们为了节省宝贵的测试时间,一般会使用梯度加压的场景策略。这时我们不知道被测环境能够达到什么样的吞吐量,也没有明确的测试指标,为了快速找到应用的最大TPS,使用梯度场景是最简单有效的。
 
所谓梯度是指开始使用较少的用户加压一段时间(几分钟即可),待TPS稳定后再继续往上加用户,如此循环,直到TPS不再增加为止。整个过程就像爬楼梯一样,所以称为“梯度”。图2是某个交易的梯度加压场景的图示,横轴是场景运行时间,纵轴是并发用户数,图中场景的策略是每个梯度加载5个用户并执行5分钟,整个场景最大并发为20,执行时间为20分钟。