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

2019 年 12 月 25 日

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

用户认证服务


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


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


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


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


@Servicepublic 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。



@Componentclass 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@EnableServiceCombpublic 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 discoveryribbon: eureka: enabled: false

microservice.yaml 中定义服务中心地址:



APPLICATION_ID: companyservice_description: name: manager version: 0.0.1cse: 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.gitcd ServiceComb-Company-WorkShop/kubernetes/bash start.sh

复制代码


Yaml 文件解读


以作者的实际环境为例:


root@zenlin:~/src/LinuxCon-Beijing-WorkShop/kubernetes# kubectl get pod -owideNAME                                      READY     STATUS    RESTARTS   AGE       IP            NODEcompany-beekeeper-3737555734-48sxf        1/1       Running   0          17s       10.244.2.49   zenlinnode2company-bulletin-board-4113647782-th91w   1/1       Running   0          17s       10.244.1.53   zenlinnode1company-doorman-3391375245-g0p8c          1/1       Running   0          17s       10.244.1.55   zenlinnode1company-manager-454733969-0c1g8           1/1       Running   0          16s       10.244.2.50   zenlinnode2company-worker-1085546725-x7zl4           1/1       Running   0          17s       10.244.1.54   zenlinnode1zipkin-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 -owideNAME CLUSTER-IP EXTERNAL-IP PORT(S) AGE SELECTORcompany-bulletin-board 10.99.70.46 <none> 30100/TCP 12m io.kompose.service=company-bulletin-boardcompany-manager 10.100.61.227 <nodes> 8083:30301/TCP 12m io.kompose.service=company-managerzipkin 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/v1beta1kind: Deploymentmetadata:  creationTimestamp: null  labels:  io.kompose.service: company-worker  name: company-workerspec:  replicas: 1  strategy: {}  template:  metadata:    creationTimestamp: null    labels:      io.kompose.service: company-workerspec:  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: Alwaysstatus: {}
复制代码


该 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 1kubectl 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,可以整理出以下的模型:



另外,经典的航空订票系统 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 Autoscalerkubectl autoscale deployment zipkin --cpu-percent=50 --min=1 --max=10kubectl autoscale deployment company-bulletin-board --cpu-percent=50 --min=1 --max=10kubectl autoscale deployment company-worker --cpu-percent=50 --min=1 --max=10kubectl autoscale deployment company-doorman --cpu-percent=50 --min=1 --max=10kubectl autoscale deployment company-manager --cpu-percent=50 --min=1 --max=10kubectl 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 启动阶段



图 2 启动阶段



图 3 过程



图 4 结果



图 5 结果



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


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


2019 年 12 月 25 日 18:10211

评论

发布
暂无评论
发现更多内容

ARTS-WEEK10

一周思进

ARTS 打卡计划

ARTS打卡Week 10

teoking

吃灰的旧显示器别扔!

Sicolas Flamel

学习 随笔杂谈

设计模式之——JDK动态代理的源码分析

诸葛小猿

动态代理 cglib 代理模式 Proxy

学了那么多技术,为何依然成不了架构师

菜根老谭

架构设计原则

Requests模块基本操作

骆俊

Java 常见的几种 OOM

hepingfly

Java OOM

计算机网络基础(十六)---传输层-可靠传输的基本原理

书旅

计算机网络 网络 协议族 网络层

那些不可貌相的代码规范

双儿么么哒

代码质量

如何理解Java8 的函数式编程

Rayjun

Java 函数式编程

LeetCode题解:21. 合并两个有序链表,迭代,JavaScript,详细注释

Lee Chen

LeetCode 前端进阶训练营

深化区块链技术的应用 体现其价值产业发展良机

CECBC区块链专委会

区块链技术 数字经济

BGP、OSPF、MPLS路由协议RFC分享

Phantasm

十年一梦,小米的原罪得到救赎了吗?

脑极体

视读——沟通的艺术,看入人里,看出人外(第二章)

双儿么么哒

读书笔记 视觉笔记

关于 Bash 的 10 个常见误解

柴锋

bash Linux DevOps Shell

Suricata-流的处理

Phantasm

网络安全 suricata flow

ARTS打卡 第11周

引花眠

ARTS 打卡计划

图文讲解 AQS ,一起看看 AQS 的源码……(图文较长)

程序员小航

AQS jdk源码 源码阅读 java 并发

当实证资产定价遇上机器学习

分析101

人工智能 学习 金融科技 金融 资产定价

微服务、DDD

chenzt

ARTS 打卡(20.07.20-20.07.26)

小王同学

面试这么撩准拿offer,HashMap深度学习,扰动函数、负载因子、扩容拆分,原理和实践验证,让懂了就是真的懂!

小傅哥

Java 面试 hashmap 负载因子 扰动函数

ARTS Week11

时之虫

ARTS 打卡计划

如何让区块链技术能够更好赋能数字社会建设

CECBC区块链专委会

区块链 数字经济

应用开发基础之-并发编程

superman

程序的机器级表示-控制

引花眠

计算机基础

十多位全球技术专家,为你献上近十个小时的.Net微服务介绍

newbe36524

微服务 .net core netcore 容器化

第十章作业

武鹏

热潮-区块链的价值能够体现在哪些方面?

CECBC区块链专委会

区块链技术 标准化 应用价值

求刚好大于当前数组组合,Code Review最佳实践,JVM框架原理,JVM垃圾回收原理 John 易筋 ARTS 打卡 Week 12

John(易筋)

Code Review ARTS 打卡计划 JVM虚拟机原理 JVM垃圾回收原理 Array算法

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