完整微服务化示例:使用 Apache ServiceComb 进行微服务开发、容器化、弹性伸缩 (五)

阅读数:29 2019 年 12 月 25 日 18:10

完整微服务化示例:使用 Apache ServiceComb 进行微服务开发、容器化、弹性伸缩(五)

用户认证服务

当用户发送非登录请求时,我们首先需要验证用户合法,在如下服务中,我们通过告示栏获取门卫联系方式, 然后发送用户 token 给门卫进行认证。

ServiceComb 提供了相应 RestTemplate 实现查询 Service Center 中的服务注册信息,只需在地址中以如下格式包含被调用的服务名

复制代码
cse://doorman/path/to/rest/endpoint

ServiceComb 将自动查询对应服务并发送请求到地址中的服务端点。

复制代码
@Service
public class AuthenticationService {
private static final Logger logger = LoggerFactory.getLogger(AuthenticationService.class);
private static final String DOORMAN_ADDRESS = "cse://doorman";
private final RestTemplate restTemplate;
AuthenticationService() {
this.restTemplate = RestTemplateBuilder.create();
this.restTemplate.setErrorHandler(new ResponseErrorHandler() {
@Override
public boolean hasError(ClientHttpResponse clientHttpResponse) throws IOException {
return false;
}
@Override
public void handleError(ClientHttpResponse clientHttpResponse) throws IOException {
}
});
}
@HystrixCommand(fallbackMethod = "timeout")
public ResponseEntity<String> validate(String token) {
logger.info("Validating token {}", token);
ResponseEntity<String> responseEntity = restTemplate.postForEntity(
DOORMAN_ADDRESS + "/rest/validate",
validationRequest(token),
String.class
);
if (!responseEntity.getStatusCode().is2xxSuccessful()) {
logger.warn("No such user found with token {}", token);
}
logger.info("Validated request of token {} to be user {}", token, responseEntity.getBody());
return responseEntity;
}
private ResponseEntity<String> timeout(String token) {
logger.warn("Request to validate token {} timed out", token);
return new ResponseEntity<>(REQUEST_TIMEOUT);
}
private HttpEntity<Token> validationRequest(String token) {
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON_UTF8);
return new HttpEntity<>(new Token(token), headers);
}
}

请求过滤

接下来我们提供 ZuulFilter 实现过滤用户请求,调用 authenticationService.validate(token) 认证用户 token。若用户合法则路由用户请求到对应服务,否则返回 403 forbidden。

复制代码
@Component
class AuthenticationAwareFilter extends ZuulFilter {
private static final Logger logger = LoggerFactory.getLogger(AuthenticationAwareFilter.class);
private static final String LOGIN_PATH = "/login";
private final AuthenticationService authenticationService;
private final PathExtractor pathExtractor;
@Autowired
AuthenticationAwareFilter(
AuthenticationService authenticationService,
PathExtractor pathExtractor) {
this.authenticationService = authenticationService;
this.pathExtractor = pathExtractor;
}
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
String path = pathExtractor.path(RequestContext.getCurrentContext());
logger.info("Received request with query path: {}", path);
return !path.endsWith(LOGIN_PATH);
}
@Override
public Object run() {
filter();
return null;
}
private void filter() {
RequestContext context = RequestContext.getCurrentContext();
if (doesNotContainToken(context)) {
logger.warn("No token found in request header");
rejectRequest(context);
} else {
String token = token(context);
ResponseEntity<String> responseEntity = authenticationService.validate(token);
if (!responseEntity.getStatusCode().is2xxSuccessful()) {
logger.warn("Unauthorized token {} and request rejected", token);
rejectRequest(context);
} else {
logger.info("Token {} validated", token);
}
}
}
private void rejectRequest(RequestContext context) {
context.setResponseStatusCode(SC_FORBIDDEN);
context.setSendZuulResponse(false);
}
private boolean doesNotContainToken(RequestContext context) {
return authorizationHeader(context) == null
|| !authorizationHeader(context).startsWith(TOKEN_PREFIX);
}
private String token(RequestContext context) {
return authorizationHeader(context).replace(TOKEN_PREFIX, "");
}
private String authorizationHeader(RequestContext context) {
return context.getRequest().getHeader(AUTHORIZATION);
}
}

最后提供服务应用入口:

复制代码
@SpringBootApplication
@EnableCircuitBreaker
@EnableZuulProxy
@EnableDiscoveryClient
@EnableServiceComb
public class ManagerApplication {
public static void main(String[] args) {
SpringApplication.run(ManagerApplication.class, args);
}
}
application.yaml 中定义路由规则:
zuul:
routes:
doorman:
serviceId: doorman
sensitiveHeaders:
worker:
serviceId: worker
beekeeper:
serviceId: beekeeper
# disable netflix eurkea since it's not used for service discovery
ribbon:
eureka:
enabled: false
microservice.yaml 中定义服务中心地址:
APPLICATION_ID: company
service_description:
name: manager
version: 0.0.1
cse:
service:
registry:
address: http://sc.servicecomb.io:30100

项目归档 (Project Archive)

经理在每次用户请求后将项目进行归档,如果将来有内容相同的请求到达,经理可以就近获取结果,不必再购买 技工和养蜂人提供的计算服务,节省公司开支。

对于归档功能的实现,我们采用了 Spring Cache Abstraction,具体细节超出了这篇文章的范围,大家如果有兴趣可以 查看 github 上 workshop 的 manager 模块代码。

人力资源 (Human Resource)

人力资源从运维层面保证服务的可靠性,主要功能有

  • 弹性伸缩,以保证用户请求量超过技工或养蜂人处理能力后,招聘更多技工或养蜂人加入项目;当请求量回落后,裁剪技工或养蜂人以节省公司开支

  • 健康检查,以保证技工或养蜂人告病时,能有替补接手任务

  • 滚动升级,以保证项目需要新技能时,能替换、培训技工或养蜂人,不中断接收用户请求

人力资源的功能需要云平台提供支持,在后续的文章中会跟大家介绍,我们如何在华为云上轻松实现这些功能。

微服务化小结

至此,我们用一个公司的组织结构作为例子,给大家介绍了微服务的完整架构,以及如何使用微服务框架 ServiceComb 快速开发微服务,以及服务间互通、契约认证。

Workshop demo 项目也包含大量完整易懂的测试 代码,以及使用 docker 集成微服务,模拟生存环境,同时应用 Travis( https://travis-ci.org/ ) 搭建持续集成环境,体现 DevOps 在微服务开发中的实践。希望能对大家有所帮助。

容器化并集群部署

现在,github( https://github.com/ServiceComb/ServiceComb-Company-WorkShop.git ) 上已经提供了在 kubernetes 集群上一键式部署的功能。本文将着重讲解相应的 yaml 文件和服务间通信,这对于开发者基于 Company 模型进行微服务开发并且部署到云上将会有所帮助。

一键部署

Run Company on Kubernetes Cluster( https://github.com/ServiceComb/ServiceComb-Company-WorkShop/blob/master/kubernetes/README.md ) 提供了详细的使用方法,读者只需通过以下 3 条指令,就可将 company 在 kubernetes 集群上部署起来,

复制代码
git clone https://github.com/ServiceComb/ServiceComb-Company-WorkShop.git
cd ServiceComb-Company-WorkShop/kubernetes/
bash start.sh

Yaml 文件解读

以作者的实际环境为例:

复制代码
root@zenlin:~/src/LinuxCon-Beijing-WorkShop/kubernetes# kubectl get pod -owide
NAME READY STATUS RESTARTS AGE IP NODE
company-beekeeper-3737555734-48sxf 1/1 Running 0 17s 10.244.2.49 zenlinnode2
company-bulletin-board-4113647782-th91w 1/1 Running 0 17s 10.244.1.53 zenlinnode1
company-doorman-3391375245-g0p8c 1/1 Running 0 17s 10.244.1.55 zenlinnode1
company-manager-454733969-0c1g8 1/1 Running 0 16s 10.244.2.50 zenlinnode2
company-worker-1085546725-x7zl4 1/1 Running 0 17s 10.244.1.54 zenlinnode1
zipkin-508217170-0khr3 1/1 Running 0 17s 10.244.2.48 zenlinnode2

可以看到,一共启动了 6 个 pod,分别为,公司经理(company-manager)、门卫(company-doorman)、公告栏(company-bulletin-board)、技工(company-worker)、养蜂人(company-beekeeper)、调用链跟踪(zipkin),K8S 集群分别为他们分配对应的集群 IP。

复制代码
root@zenlin:~/src/LinuxCon-Beijing-WorkShop/kubernetes# kubectl get svc -owide
NAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTOR
company-bulletin-board 10.99.70.46 <none> 30100/TCP 12m io.kompose.service=company-bulletin-board
company-manager 10.100.61.227 <nodes> 8083:30301/TCP 12m io.kompose.service=company-manager
zipkin 10.104.92.198 <none> 9411/TCP 12m io.kompose.service=zipkin

仅启动了 3 个 service,调用链跟踪(zipkin)、公告栏(company-bulletin-board)以及经理(company-manager),这是因为,调用链跟踪和公告栏需要在集群内被其他服务通过域名来调用,而经理需要作为对外作为网关,统一暴露服务端口。

查看 company-bulletin-board-service.yaml 文件,

复制代码
apiVersion: v1
kind: Service
metadata:
creationTimestamp: null
labels:
io.kompose.service: company-bulletin-board
name: company-bulletin-board
spec:
ports:
- name: "30100"
port: 30100
targetPort: 30100
selector:
io.kompose.service: company-bulletin-board
status:
loadBalancer: {}

该文件定义了公告栏对应的 service,给 service 定义了 name、port 和 targetPort,这样通过 kubectl expose 创建的 service 会在集群内具备 DNS 能力,在其他服务刚启动还未注册到公告栏(服务注册发现中心)时,就是使用该能力来访问到公告栏并注册服务的。

对于 label 和 selector 的作用,在一个 service 启动多个 pod 的场景下将会非常有用,当某个 pod 崩溃时,服务的 selector 将会自动将死亡的 pod 从 endpoints 中移除,并且选择新的 pod 加入到 endpoints 中。

查看 company-worker-deployment.yaml 文件,

复制代码
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
creationTimestamp: null
labels:
io.kompose.service: company-worker
name: company-worker
spec:
replicas: 1
strategy: {}
template:
metadata:
creationTimestamp: null
labels:
io.kompose.service: company-worker
spec:
containers:
- env:
- name: ARTIFACT_ID
value: worker
- name: JAVA_OPTS
value: -Dcse.service.registry.address=http://company-bulletin-board:30100 -Dservicecomb.tracing.collector.adress=http://zipkin:9411
image: servicecomb/worker:0.0.1-SNAPSHOT
name: company-worker
ports:
- containerPort: 7070
- containerPort: 8080
resources: {}
restartPolicy: Always
status: {}

该 yaml 文件定义了副本数为 1(replicas: 1)的 pod,可以通过修改该副本数控制所需启动的 pod 的副本数量(当然也可以使用 K8S 的弹性伸缩能力去实现按需动态水平伸缩,弹性伸缩部分将在后面的博文中提供)。前面我们提到过 company-bulletin-board 具备了 DNS 的能力,故现在可以通过该 Deployment 中的 env 传递 cse.service.registry.address 的值给 pod 内的服务使用,如:-Dcse.service.registry.address=http://company-bulletin-board:30100,kube-dns( https://github.com/kubernetes/kubernetes/blob/master/cluster/addons/dns/README.md ) 将会自动解析该 servicename。

对于 kubernetes 如何实现服务间通信,可以阅读 connect-applications-service( https://kubernetes.io/docs/concepts/services-networking/connect-applications-service/ )。

其他的 deployment.yaml 以及 service.yaml 都跟以上大同小异,唯一例外的是 company-manager 服务,我们可以看到在 company-manager-service.yaml 中看到定义了 nodePort,这将使能 company-manager 对集群外部提供公网 IP 和服务端口,如下:

复制代码
spec:
ports:
- name: "8083"
port: 8083
targetPort: 8080
nodePort: 30301
protocol: TCP
type: NodePort

可以通过以下方法获得公网 IP 和服务端口:

复制代码
kubectl get svc company-manager -o yaml | grep ExternalIP -C 1
kubectl get svc company-manager -o yaml | grep nodePort -C 1

接下来你就可以使用公网 IP 和服务端口访问已经部署好的 company 了,在 github.com/ServiceComb/ServiceComb-Company-WorkShop/kubernetes( https://github.com/ServiceComb/ServiceComb-Company-WorkShop/blob/master/kubernetes/README.md ) 上详细提供了通过在集群内访问和集群外访问的方法。

模型归纳

通过详细阅读所有的 deployment.yaml 和 service.yaml,可以整理出以下的模型:

完整微服务化示例:使用 Apache ServiceComb 进行微服务开发、容器化、弹性伸缩(五)

另外,经典的航空订票系统 Acmeair 也已经支持在 kubernetes 上一键式部署基于 ServiceComb 框架开发的版本,点击访问 Run Acmeair on Kubernetes( https://github.com/WillemJiang/acmeair/tree/master/kubernetes ) 获取 。

本小节将继续在 K8S 上演示使用 K8S 的弹性伸缩能力进行 Company 示例的按需精细化资源控制,以此体验微服务化给大家带来的好处。

环境准备

K8S 环境准备:

为使 K8S 具备弹性伸缩能力,需要先在 K8S 中安装监控器 Heapster 和 Grafana:

具体读者踩了坑后更新的 heapster( https://github.com/zenlinTechnofreak/LinuxCon-Beijing-WorkShop/tree/autoscal/kubernetes/heapster/deploy ) 的安装脚本作者放在:heapster,可直接获取下载获取,需要调整一个参数,后直接运行 kube.sh 脚本进行安装。

复制代码
vi LinuxCon-Beijing-WorkShop/kubernetes/heapster/deploy/kube-config/influxdb/heapster.yaml
spec:
replicas: 1
template:
metadata:
labels:
task: monitoring
k8s-app: heapster
spec:
serviceAccountName: heapster
containers:
- name: heapster
image: gcr.io/google_containers/heapster-amd64:v1.4.1
imagePullPolicy: IfNotPresent
command:
- /heapster
#集群内安装直接使用 kubernetes
- --source=kubernetes
#集群外安装请直接将下面的服务地址替换为 k8s api server 地址
# - --source=kubernetes:http://10.229.43.65:6443?inClusterConfig=false
- --sink=influxdb:http://monitoring-influxdb:8086

启动 Company:

下载 Comany 支持弹性伸缩的代码:

复制代码
git clone https://github.com/ServiceComb/ServiceComb-Company-WorkShop.git
cd LinuxCon-Beijing-WorkShop/kubernetes/
bash start-autoscale.sh

在 Company 的 deployment.yaml 中, 增加了如下限定资源的字段,这将限制每个 pod 被限制在 200mill-core(1000 毫 core == 1 core) 的 cpu 使用率以内。

复制代码
resources:
limits:
cpu: 200m

在 start-autoscale.sh 中,对每个 deployment 创建 HPA(pod 水平弹性伸缩器) 资源,限定每个 pod 的副本数弹性伸缩时控制在 1 到 10 之间,并限定每个 pod 的 cpu 占用率小于 50%,结合前面限定了 200mcore,故,每个 pod 的的平均 cpu 占用率会被 HPA 通过弹性伸缩能力控制在 100mcore 以内。

复制代码
# Create Horizontal Pod Autoscaler
kubectl autoscale deployment zipkin --cpu-percent=50 --min=1 --max=10
kubectl autoscale deployment company-bulletin-board --cpu-percent=50 --min=1 --max=10
kubectl autoscale deployment company-worker --cpu-percent=50 --min=1 --max=10
kubectl autoscale deployment company-doorman --cpu-percent=50 --min=1 --max=10
kubectl autoscale deployment company-manager --cpu-percent=50 --min=1 --max=10
kubectl autoscale deployment company-beekeeper --cpu-percent=50 --min=1 --max=10

当运行 start-autoscale.sh 之后,具备弹性伸缩器的 company 已经被创建,可通过下面指令进行 HPA 的查询:

复制代码
kubectl get hpa

启动压测:

复制代码
export $HOST=<heapster-ip>:<heapster-port>
bash LinuxCon-Beijing-WorkShop/kubernetes/stress-test.sh

该脚本不断循环执行 1s 内向 Company 请求计算 fibonacci 数值 200 次,对 Company 造成请求压力:

复制代码
FIBONA_NUM=`curl -s -H "Authorization: $Authorization" -XGET "http://$HOST/worker/fibonacci/term?n=6"`

测试过程与结果

分别查看 HPA 状态以及 Grafana,如下:

图 1 启动阶段

完整微服务化示例:使用 Apache ServiceComb 进行微服务开发、容器化、弹性伸缩(五)

图 2 启动阶段

完整微服务化示例:使用 Apache ServiceComb 进行微服务开发、容器化、弹性伸缩(五)

图 3 过程

完整微服务化示例:使用 Apache ServiceComb 进行微服务开发、容器化、弹性伸缩(五)

图 4 结果

完整微服务化示例:使用 Apache ServiceComb 进行微服务开发、容器化、弹性伸缩(五)

图 5 结果

完整微服务化示例:使用 Apache ServiceComb 进行微服务开发、容器化、弹性伸缩(五)

从以上过程可以分析出,以下几点:

1. 压力主要集中在 company-manager 这个 pod 上,K8S 的 autoscaler 通过弹性增加该 pod 的副本数量,最终达到目标:每个 pod 的 cpu 占用率低于限定值的 50%(图 5,Usage default company-manager/Request default company-manager = 192/600 约等于图 4 中的 33%),并保持稳定。

2. 在弹性伸缩过程中,在还没稳定前可能造成丢包,如图 3。

3.Company 启动会导致系统资源负载暂时性加大,故 Grafana 上看到的 cpu 占用率曲线会呈现波峰状,但随着系统稳定运行后,HPA 会按照系统的稳定资源消耗准确找到匹配的副本数。图 3 中副本数已超过实际所需 3 个,但随着系统稳定,最终还是稳定维持在 3 个副本。

4. 在 HPA 以及 Grafana 可以看到缩放和报告数据都会有延迟,按照官方文档说法,只有在最近 3 分钟内没有重新缩放的情况下,才会进行放大。从最后一次重新缩放,缩小比例将等待 5 分钟。而且,只有在 avg/ Target 降低到 0.9 以下或者增加到 1.1 以上(10%容差)的情况下,才可能会进行缩放。

以上,就是本次对 Compan 示例弹性伸缩的全过程,Martin Fowler 在 2014 年 3 月的文章中提到:

微服务是一种架构风格,一个大型复杂软件应用由一个或多个微服务组成。系统中的各个微服务可被独立部署,各个微服务之间是松耦合的。每个微服务仅关注于完成一件任务并很好地完成该任务。在所有情况下,每个任务代表着一个小的业务能力。

国内实践微服务的先行者王磊先生也在《微服务架构与实践》一书中进行了全面论述。

Company 使用 ServiceComb 进行微服务化改造后,具备了微服务的属性,故可以对单个负载较大的 company-manager 这个微服务进行精细化的控制,达到按需的目的,相比传统单体架构来讲,这将大大帮助准确有效地化解应用瓶颈,提高资源的利用效率。

本文转载自 微服务 开源项目 Apache ServiceComb 官网博客:

http://servicecomb.incubator.apache.org/cn/docs/linuxcon-workshop-demo/

http://servicecomb.incubator.apache.org/cn/docs/company-on-kubernetes/

http://servicecomb.incubator.apache.org/cn/docs/autoscale-on-company/

本文转载自微服务蜂巢公众号。

原文链接: https://mp.weixin.qq.com/s?__biz=MzUxNTEwNTg5Mg==&mid=2247488670&idx=1&sn=7556990599cea8e0fd80ed54b9f39026&chksm=f9bae195cecd688332940d0b296909e8130d828104c36c20f49d76d43561f626eae24bd0c1ef&scene=0&xtrack=1&key=17fbc717c1803f309d535a4bdbc0f9c0d8d2089a5c7e455df8c0a5e6d824a6534b8476d2ad151cd45ed10eb7a15577914596444e867d96f615c277fc05fe951cef48de8b7d0732dcb4bc74f5c0e2f95a&ascene=14&uin=MTI5MjAyNjcyMQ%3D%3D&devicetype=Windows+10&version=62070158&lang=zh_CN&exportkey=AfH3CzqE%2F1ENttvg815y9Uo%3D&pass_ticket=oGcazNeaRfkuszcDU0L7jpfeTFZ3%2FULBAbPnhurUkiyW7DLvBVsoC%2Fh5OWX1zIsH

评论

发布