服务设计会影响到业务需求是否被正确、高效地实现,良好的服务设计能够帮助领域专家与开发人员之间,以及团队内部进行高效、准确的沟通。良好的实现则能缩短服务上线的周期,并提升可扩展性及可维护性。
在微服务架构设计的过程中,架构设计、接口设计需要和代码库一样,使用版本化机制管理起来。同时,作为独立的服务,应该能够在本地运行,并且要有完善并且容易理解的文档,方便团队成员快速上手。在本书的第3章中,笔者已经阐述了服务划分、服务内部实现、通信机制等内容,在本节中,我们将重点探讨服务设计与实现过程中的相关实践。
5.3.1 架构即代码
当探索整个系统的服务设计时,我们总会求助于它的架构图。单体应用架构下,功能以组件/模块化的形式存在,比较容易在一张架构图上体现出来。而在微服务架构下,服务数量多,很难以一种完整的状态体现在架构图上。
另外,系统的架构图通常都是使用Visio等类似的二进制工具绘制而成的,保存为二进制文件,维护起来不太方便。
在笔者的微服务实践中,通常的思路是使用Graphviz工具,基于dot语言来描述架构图,并将其保存为SVG、PNG,或者PDF之类的格式。同时,这些dot语言代码可以放在微服务的代码库中,以版本化的方式保存起来,任何的架构变动都能有效地反映到代码库中。Graphviz的安装非常简单,在Mac系统下通过brew install graphviz安装,在Windows系统下可以下载软件包安装。
譬如,假设我们现在需要为一个社交网站绘制架构图,前端的Web应用通过API Gateway请求照片、聊天、朋友服务,则其代码实现如下,架构示意图如图5-4所示。
digraph architecture {
rankdir=LR;
... # 节点定义如api_gateway、photo等,形状设置、填充颜色等
subgraph front_app {
front -> {api_gateway};
}
subgraph api_gateway {
api_gateway ->{photo, chat, friend};
}
subgraph microservices {
photo -> {database};
chat -> {database,cache};
friend -> {database,weibo_api};
}
subgraph dbs {
cache -> database;
}
}
图5-4 用Graphviz生产的架构图
使用Graphviz实现架构图,代码很容易理解。也可以直接使用GraphvizGUI的工具,实时看到修改的结果,非常方便。类似的工具还有Mermaid(https://github.com/knsv/mermaid),其也是一个类似的工具,而DSL更加简洁一些。除此之外,有些在线工具,如draw.io、gliffy,也都是不错的绘制微服务架构图的工具。
5.3.2 接口即代码
当接触一个新的服务时,我们往往要花一些时间去学习服务提供的 API,并通过案例了解用法。通常这样的API接口介绍,都是记录在文档中,而这样的文档往往得不到很好的更新与维护。在微服务架构下,随着服务数量增加,接口变化频繁,对应的接口管理也变得更加困难。
在笔者实现微服务的过程中,通常是通过Swagger这种工具可视化REST API的规范。通过SwagerEditor,能方便地定义API的详细信息、HTTP请求方法,以及请求响应等内容,如图5-5所示。
图5-5 Swagger案例
另外,可以将Swagger生成的YAML保存在微服务的代码库中,其相当于将接口的文档以版本化的方式管理起来;如果是使用Java实现的服务,也可以引入swagger-springmvc插件,其内嵌了Swagger-UI,直接访问即可查看服务的API接口信息。
在本书的实战篇中,笔者将介绍ServiceComb框架,它支持通过类Swagger的契约文件定义接口,使用方便,同时也达到了将接口定义版本化管理的目的。
5.3.3 本地运行服务
开发人员可以直接将单体应用在本地运行起来,进行端到端的功能验证。在微服务架构下,服务虽然能独立启动,但是要在本地进行端到端的验证,可能需要启动依赖的服务、数据库等。同时,微服务可能采用不同的技术实现,准备和维护这些服务的运行环境也有一定的时间成本。笔者通常会使用Docker/docker-compose的方式,结合自动化的脚本来解决微服务在本地运行的问题。
对于纯前端的工程,可以使用如npmstart这样的方式直接启动服务,依赖的服务可以配置为测试环境或者预生产环境,如果没有写操作,甚至都可以使用生产环境。如果服务不支持跨站请求,那么可能需要利用Docker/docker-compose在本地启动依赖的微服务、修改配置,并支持跨站请求。
对于后端工程,在本地运行服务的需求主要有两个,一个是在IDE中调试代码,还有一个就是验证开发的功能是否满足需求。有时在本地运行端到端的测试,也需要在本地启动微服务以及关联的服务。比如在一个 Java 的微服务开发过程中,笔者为团队准备了两个docker-compose的YAML文件来组织服务在本地的构建与运行,分别用于为IDE调试和本地功能自验。比如下面的debug.yml文件,用来组织微服务依赖的注册中心、数据库和redis的环境:
version:"2"
services:
servicecenter:
image:service-center
network_mode:"host"
logging:
driver:"none"
redis:
image: redis:4.0.2-alpine
network_mode:"host"
postgre:
image: postgres:9.3-alpine
network_mode:"host"
这里直接的容器都直接使用了host网络模型,从而避免了额外的link配置。启动包含本地代码修改的微服务的compose.yml文件如下:
version:"2"
services:
application:
build:
context: ./
environment:
...
network_mode:"host"
servicecenter:
image: service-center
network_mode:"host"
logging:
driver:"none"
redis:
image: redis:4.0.2-alpine
network_mode:"host"
postgre:
image: postgres:9.3-alpine
network_mode:"host"
用两个简单的shell脚本包装,用来启动调试环境的脚本:
#!/bin/bash
#debug.sh
docker stop $(docker ps -q)
docker-compose -f debug.yml up -d
本地启动所有相关服务的脚本:
#!/bin/bash
#compose.sh
docker stop $(docker ps -q)
docker-compose -f compose.yml up -d
通过在本地启动调试和自验的环境,可以很方便的在本地调试、验证功能,以及在本地运行端到端的测试,让开发人员可以及早自测功能,保证实现以满足需求。
5.3.4 OnePage文档
采用微服务意味着拥抱快速变化,这种快速变化反映在技术栈、架构、人员的变化,如何提升这种变化的可见性,如何让新人或者不熟悉服务的人能快速了解、上手微服务,也是微服务实施面临的一个挑战。
通过将服务相关的开发、测试、环境的基本信息记录在文档中,并和服务的代码库保存在一起,降低微服务的学习成本。通常笔者将这类文档称为“OnePage文档”。
OnePage文档的内容通常包括如下部分:
通常笔者会用MarkDown格式来记录这样的文档,并以README.md的形式保存在微服务的代码库中。这样任何人访问代码库时都可以快速了解该服务完整的信息。
组织良好、内容充实的OnePage文档,可以帮助我们快速上手一个新的系统,并且以最短的时间找到解决问题的思路和方法。另外,由于代码与文档在同一代码库中,对于像接口、架构等变更,也能有效保证代码与文档同步更新,较低维护成本。
5.3.5 前后端分离
系统有时需要提供界面完成与用户的交互,那么当后台服务化后,用户界面的部分如何处理呢?常见的处理机制有两种:
微服务中包含UI的方式也称作微前端(MicroFrontends),ThoughtWorks在2016年的技术雷达中首次提出了这个术语,不过目前仍处于评估阶段,因此只建议综合评估后再决定是否使用。
前后端分离综述
前后端分离的优点有以下几个:
在使用前后端分离时,需要注意以下问题:
前后端分离案例
笔者经历的一个前后端分离的案例:系统需要增加一个单独的页面来收集用户信息,在具体实现时采用了前后端分离的策略,前端提供交互,后端提供数据。其中前端采用Grunt作为构建框架,Karma作为单元测试框架,Phantomjs作为功能测试框架,同时引入了契约测试来保证API的一致性。后端微服务基于Twitter的开源框架Finagle实现。其前端工程的基本架构如图5-6所示。
图5-6 部署在S3上的前端工程基本架构
上面的例子将前端代码部署在AWSS3文件服务器上,也可以选择将其部署在其他公有云的文件服务中(如华为云的OBS服务)。自建机房可以选择在Nginx、Haproxy等HTTP服务器上部署前端代码。
5.3.6 微服务与安全
当越来越多的个人数据保存在服务器端、云服务上时,这些数据的安全就变得尤为重要。对于企业来说,出现安全问题除了会造成经济损失,也会对业务稳定性和用户信心造成重大打击。相比单体应用,微服务架构下的安全更加重要,因为它面临着更多新的挑战,除了身份认证与鉴权之外,还包括如下两方面的挑战:
除此之外,微服务的交付过程通常采用持续集成,也需要考虑在这个过程中的安全,还有在自动化部署、配置管理等方面也需要注意安全。
微服务实现时的安全
在世界质量报告(WorldQuality Report)中曾提到,大部分(约80%)的安全问题都出现在应用层。所以微服务在设计实现的时候,要特别注意安全方面的问题,尤其是在OWASPTop 10中提到的常见安全问题。
2017年最新的OWASP Top 10中最常见的应用安全问题包括注入、失效的身份认证、敏感信息泄露、XML外部实体(XXE)、失效的访问控制、安全配置错误、跨站脚本(XSS)、不安全的反序列化、使用含有已知漏洞的组件以及不足的日志记录和监控。
很多微服务开发框架,都提供了与安全相关的组件或者功能,比如在SpringSecurity、Rails中集成了CSRF防范的功能,通过引入这些组件或者功能,可以对微服务起到最基本的保护作用。
除利用框架支持的功能外,在微服务设计实现还可以采取以下措施来增强安全性:
保证第三方依赖安全
微服务的技术异构性给我们带来了技术选择的自由,使用不同语言(Java、Python、Nodejs、Ruby)和框架来实现服务。而其带来的一个问题是各种不同的第三方依赖迅速膨胀,任一依赖出现安全问题都会威胁服务本身的安全。所以,掌握微服务都有哪些第三方依赖很重要,这样才能在发现安全漏洞时及时找到使用该类库的微服务并更新以解决漏洞。
笔者曾经针对不同的语言定制过依赖收集的插件(如Java、Scala的Maven插件、Nodejs模块等),将它们引入到微服务的依赖中,并在持续集成流水线上执行对应的任务,将依赖以JSON的形式导出并发布到文件存储服务中。这些JSON文件会包含微服务的信息,以及第三方依赖列表、版本号等。实际上利用了语言的构建工具列举依赖的功能,如 mvndependency:tree,解决并循环依赖可能导致的性能问题,合入微服务相关信息即可。
这样的好处在于,当有第三方类库出现安全漏洞时,可以迅速地从这些JSON文件中找到微服务是否用到这些类库,目前的版本是否存在问题。如果需要更新,哪个微服务需要更新,1 分钟内就可以做出决策。然后将升级的任务划分到团队中,提交代码、自动化部署之后就可以完成漏洞的更新。
密码策略
最近几年国内出现了好几次拖库事件,因为网站未对用户的密码加密,导致用户信息泄露。这种情况下,用户得知信息泄露的时间滞后,很难在利益受损前更新密码。
一种应对的策略是采用加密哈希函数(CryptographicHash Function)加密用户密码后保存到数据库。比如MySQL的PASSWORD函数就采用了md5/sha-1(和MySQL的版本相关)的单向散列函数进行密码加密。好处是即便数据库被拖库,采用暴力破解或者字典攻击的方式很难或者需要花费比较长的时间才能获得用户的明文密码,这就给用户争取了重新设置密码的时间,修复安全问题。
当然,攻击者可以加密哈希函数预先计算值并生成彩虹表(RainbowTable)。攻击者如果猜到了哈希函数的算法,比如MySQL用md5或者sha-1,黑客就可以以空间换时间的方式,先计算出密码的哈希值,然后反查密码,随着这个表的增长,破解的难度可能会降低,时间也会减少,最快可以在O(1)的时间复杂度内破解密码。
要增加破解的难度,让我们能在密码泄露时多争取点时间,其中的一种方式是给密码中加点“盐”(salt),在生成密码的哈希值时,加入一个随机的字符串,然后保存在数据库中。这样对于相同的密码而言,在数据库中的保存的记录也是不同的。对于使用彩虹表破解的攻击者来说,因为需要猜测混合salt的算法,破解的成本很高。同样,暴力破解也变得不太可能。
在密码比较时,计算用户输入的哈希值,然后和数据库中去掉salt的部分记录比较,就可以验证是否是合法用户了。我们以Python下的bcrypt为例:
>>> hashed_password = bcrypt.hashpw("password",bcrypt.gensalt())
>>> print hashed_password
$2b$12$K947InrSXM6XvNoErbAcj.K5YQ/OSVvJ802MxSWNgXdrjmru8Grs2
这是用Blowfish密码生成的哈希值字符串再进行base64编码的结果,共分为4个部分,第一部分$2b$表明这是bcrypt格式的哈希;第二部分是成本(cost)值,默认是12;第三部分是22位的字符串,也就是salt的值;剩下的部分就是密码哈希后的base64编码的值。在比较的时候,只需要把哈希后的密码当作salt传进去就可以了。
Blowfish是一个symmetric-key块分组密码,对称性key的意思用同样的加密秘钥去加密解密,就像谍战片中用相同的密码本解密消息。块分组的意思就是将明文分为固定长度的块,用秘钥分别加密后再拼起来,并且密文应该和明文的长度相同。
另外一种加强密码的强度的方式是给密码撒一把“胡椒”(pepper),简单来说就是在微服务中配置特殊的字符串,将用户的密码和这个字符串一起哈希,这样可以变相地增强简单密码的强度,如下所示:
>>> bcrypt.hashpw("password*{abcd&",bcrypt.gensalt())
'$2b$12$2dVYv2o5vw6uMYe2IT9V9uWfIR2zdkpKDagNRZ8eFOpS4nyNHJuz.'
*{abcd&就是pepper,它以配置的形式保存在服务器上,主要针对的场景是数据库暴露,但是应用服务器安全,可以拖延字典攻击的时间,给用户争取足够的时间更改密码。
这里笔者使用Python代码介绍了如何在密码中以“撒盐”和“胡椒”的方式进行操作,实际上主流的语言,如Java等都有这些算法的实现,可以直接使用。
保证微服务基础设施的安全
在基础设施方面,与微服务安全相关的主要是网络、服务器以及容器安全。网络方面主要是做好隔离和访问控制,服务器的安全方面是需要主动监测并且及时修复漏洞,容器方面主要考虑权限、资源控制。
网络安全:
服务器的安全:
可以通过主动扫描、入侵检测、做好SSH密钥管理等方式来保证。
容器安全:
其他安全方面的实践
鉴于安全的重要性,应当在微服务的整个生命周期中都嵌入对安全的考量。比如在微服务的设计过程中,需要安全专家的参与,考虑架构的安全性,在实现的过程中也需要考虑编码的安全性,此外还有测试、部署维护等方面。这样的实践也被称为安全内建(BuildSecurity In)。
做到安全内建,除了上面介绍的部分,还需要在以下方面考虑安全问题:
在微服务架构下,安全是需要重点考虑的因素。在处理安全问题时,要兼顾业务交付的速度,合理地安排安全问题修复的优先级,通过良好的实践,如不可变部署、基于镜像部署等方式,降低安全问题修复的成本。从而实现业务交付和安全的双赢。