2.2 实验的应用 |
|
为了充分展示Istio的功能,我们使用不同的语言来模拟数个微服务,服务之间存在相应的调用关系,服务之间通过HTTP协议通信。我们并没有写一个实际的综合应用,例如:购物网站、论坛等,来模拟生产环境的情况,我们只是简单地模拟服务间的调用关系来进行Istio相关的功能实验,目的是通过演示Istio相关功能来学习Istio。每个服务以其使用的编程语言为服务名,例如:使用Python语言编写的服务命名为service-python。各服务的调用关系如图2-1所示。
service-js服务是一个由Vue/React实现的前端应用,当用户访问前端Web页面时,用户会看到一个静态页面。当用户点击相应的按钮时,前端页面会通过浏览器异步请求后端service-python服务提供的API接口,service-python调用后端service-lua服务和service-node服务,而service-node服务又会调用service-go服务,最终,所有服务配合来完成用户的请求,并把结果合并处理之后发送给前端浏览器。当前端页面收到请求的响应数据时会渲染出新的页面呈现给用户。
图2-1 实验应用架构
应用架构说明:
·本应用采用当前比较流行的前后端分离架构。
·前端项目使用Vue/React实现。
·前端调用Python实现的API接口。
·Python服务调用后端Node实现的服务和Lua实现的服务。
·Node服务调用Go实现的服务。
1.service-js服务
service-js服务分别使用Vue和React各实现一套Web界面,主要用于服务路由中的A/B测试,可以让不同的终端用户看到不同的前端Web界面。service-js服务主要负责根据service-python服务的响应数据,使用ECharts图表库在浏览器上展示出后端服务的具体调用关系和各个服务的调用耗时,具体的代码在实验源码根目录的service/js目录下。
v1版本使用React框架实现,源码目录如下:
.├── Dockerfile├── package.json├── package-lock.json├── public│ ├── favicon.ico│ ├── index.html│ └── manifest.json├── README.md└── src ├── App.css ├── App.js ├── App.test.js ├── index.css ├── index.js ├── logo.svg └── registerServiceWorker.js
v2版本使用Vue框架实现,源码目录如下:
.├── build│ ├── build.js│ ├── check-versions.js│ ├── logo.png│ ├── utils.js│ ├── vue-loader.conf.js│ ├── webpack.base.conf.js│ ├── webpack.dev.conf.js│ └── webpack.prod.conf.js├── config│ ├── dev.env.js│ ├── index.js│ └── prod.env.js├── Dockerfile├── index.html├── package.json├── package-lock.json├── README.md├── src│ ├── App.vue│ ├── assets│ │ └── logo.png│ ├── components│ │ └── HelloWorld.vue│ └── main.js└── static
用于容器化的Dockerfile文件如下所示:
FROM node:8-alpine LABEL maintainer="will835559313@163.com" COPY . /app WORKDIR /app RUN npm i && npm run build \ && rm -rf ./node_modules \ && npm install -g serve EXPOSE 80 CMD ["serve", "-s", "build", "-p", "80"]
2.service-python服务
service-python服务是一个用Python编写的API服务,负责接收前端的API请求,调用整合后端其他服务的响应数据,返回给前端使用。service-python服务分别使用Python2和Python3实现了两个版本的服务,具体代码在实验源码根目录的service/python目录下。service-python服务使用Flask框架实现,具体源码如下:
1 import time 2 import requests 3 from functools import partial 4 from flask import Flask, jsonify, g, request 5 from multiprocessing.dummy import Pool as ThreadPool 6 7 8 app = Flask(__name__) 9 10 11 def getForwardHeaders(request): 12 headers = {} 13 incoming_headers = [ 14 'x-request-id', 15 'x-b3-traceid', 16 'x-b3-spanid', 17 'x-b3-parentspanid', 18 'x-b3-sampled', 19 'x-b3-flags', 20 'x-ot-span-context' 21 ] 22 23 for ihdr in incoming_headers: 24 val = request.headers.get(ihdr) 25 if val is not None: 26 headers[ihdr] = val 27 28 return headers 29 30 31 @app.before_request 32 def before_request(): 33 g.forwardHeaders = getForwardHeaders(request) 34 35 36 def get_url_response(url, headers={}): 37 try: 38 start = time.time() 39 resp = requests.get(url, headers=headers, timeout=20) 40 response_time = round(time.time() - start, 2) 41 data = resp.json() 42 data['response_time'] = response_time 43 except Exception as e: 44 print(e) 45 data = None 46 return data 47 48 49 @app.route("/env") 50 def env(): 51 service_lua_url = 'http://' + 'service-lua' + '/env' 52 service_node_url = 'http://' + 'service-node' + '/env' 53 54 services_url = [service_lua_url, service_node_url] 55 pool = ThreadPool(2) 56 wrap_get_url_response = partial(get_url_response, headers=g.forwardHeaders) 57 results = pool.map(wrap_get_url_response, services_url) 58 upstream = [r for r in results if r] 59 60 return jsonify({ 61 "message": 'python v1', 62 "upstream": upstream 63 }) 64 65 66 @app.route("/status") 67 def status(): 68 return "ok" 69 70 71 if __name__ == '__main__': 72 app.run(host='0.0.0.0', port=80)
第11~28行定义的getForwardHeaders函数是为了从请求中提取出用于Istio调用链追踪的头信息,用于传递给service-python要调用的其他后端服务。
第31~33行表示每个请求在被处理前,调用getForwardHeaders函数,从请求中提取出Istio调用链追踪的头信息,并保存到全局对象g的forwardHeaders变量中。
第36~46行定义了用于获取后端服务响应数据的get_url_response函数。
第49~63行定义了真正的业务路由,使用线程池的方式并发请求后端服务,并把后端服务的响应数据组合处理后返回给调用方。
第66~68行定义了用于服务健康检查的路由。
第71~72行表示服务启动在0.0.0.0地址的80端口。
v1版本和v2版本源码只有第61行有略微差别,在v2版本中"python v1"修改为"python v2"。
用于容器化的Dockerfile文件如下所示:
FROM python:2-alpine LABEL maintainer="will835559313@163.com" COPY . /app WORKDIR /app RUN pip install -r requirements.txt CMD [ "python", "main.py" ]
v1版本和v2版本的Dockerfile只有使用的基础镜像版本不同,其他保持一致。
3.service-lua服务
service-lua服务使用OpenResty的不同版本用Lua语言分别实现了两个版本的服务。具体的代码在实验源码根目录的service/lua目录下。源代码如下:
1 worker_processes 4; 2 error_log logs/error.log; 3 events { 4 worker_connections 10240; 5 } 6 http { 7 server { 8 listen 80; 9 location / { 10 default_type text/html; 11 content_by_lua ' 12 ngx.say("hello, world") 13 '; 14 } 15 16 location = /status { 17 default_type text/html; 18 content_by_lua ' 19 ngx.say("ok") 20 '; 21 } 22 23 location = /env { 24 charset utf-8; 25 charset_types application/json; 26 default_type application/json; 27 content_by_lua ' 28 json = require "cjson" 29 ngx.status = ngx.HTTP_OK 30 version = "lua v1" 31 data = { 32 message = version 33 } 34 ngx.say(json.encode(data)) 35 return ngx.exit(ngx.HTTP_OK) 36 '; 37 } 38 } 39 }
第8行表示服务启动在0.0.0.0地址的80端口。
第9~14行定义了访问服务的/链接时返回"hello world"。
第16~21行定义了用于服务健康检查的/status链接。
第23~37行定义了真正的业务逻辑,当访问/env链接时,响应服务的版本信息。
v1版本和v2版本源码只有第30行有略微差别,在v2版本中"lua v1"修改为"lua v2"。
用于容器化的Dockerfile文件如下所示:
FROM openresty/openresty:1.11.2.5-alpine LABEL maintainer="will835559313@163.com" COPY . /app WORKDIR /app EXPOSE 80 ENTRYPOINT ["/usr/local/openresty/bin/openresty", "-c", "/app/nginx.conf", "-g", "daemon off;"]
v1版本和v2版本的Dockerfile只有使用的基础镜像版本不同,其他保持一致。
4.service-node服务
service-node服务使用Node的不同版本分别实现了两个版本的服务。具体的代码在实验源码根目录的service/node目录下。源代码如下:
1 const Koa = require('koa'); 2 const Router = require('koa-router'); 3 const axios = require('axios') 4 const app = new Koa(); 5 const router = new Router(); 6 7 8 function getForwardHeaders(request) { 9 headers = {} 10 incoming_headers = [ 11 'x-request-id', 12 'x-b3-traceid', 13 'x-b3-spanid', 14 'x-b3-parentspanid', 15 'x-b3-sampled', 16 'x-b3-flags', 17 'x-ot-span-context' 18 ] 19 20 for (idx in incoming_headers) { 21 ihdr = incoming_headers[idx] 22 val = request.headers[ihdr] 23 if (val !== undefined && val !== '') { 24 headers[ihdr] = val 25 } 26 } 27 return headers 28 } 29 30 31 32 router.get('/status', async (ctx, next) => { 33 ctx.body = 'ok'; 34 }) 35 36 router.get('/env', async (ctx, next) => { 37 forwardHeaders = getForwardHeaders(ctx.request) 38 service_go_url = 'http://' + 'service-go' + '/env' 39 upstream_ret = null 40 try { 41 let start = Date.now() 42 const response = await axios.get(service_go_url, { 43 headers: forwardHeaders, 44 timeout: 20000 45 }); 46 response_time = ((Date.now() - start) / 1000).toFixed(2) 47 upstream_ret = response.data 48 upstream_ret.response_time = response_time 49 } catch (error) { 50 console.error('error'); 51 } 52 if (upstream_ret) { 53 ctx.body = { 54 'message': 'node v1', 55 'upstream': [upstream_ret] 56 }; 57 } else { 58 ctx.body = { 59 'message': 'node v1', 60 'upstream': [] 61 } 62 } 63 }) 64 65 app.use(router.routes()).use(router.allowedMethods()); 66 app.listen(80);
第8~28行定义的getForwardHeaders函数是为了从请求中提取出用于Istio调用链追踪的头信息,用于传递给service-node要调用的其他后端服务。
第32~34行定义了用于服务简单健康检查的路由。
第36~63行定义了真正的业务路由,请求后端服务,并把后端服务的响应数据组合处理后返回给调用方。
第65~66行表示服务启动在0.0.0.0地址的80端口。
v1版本和v2版本源码只有第54和59行有略微差别,在v2版本中"node v1"修改"node v2"。
用于容器化的Dockerfile文件如下:
FROM node:8-alpine LABEL maintainer="will835559313@163.com" COPY . /app WORKDIR /app RUN npm i EXPOSE 80 CMD ["node", "main.js"]
v1版本和v2版本的Dockerfile只有使用的基础镜像版本不同,其他保持一致。
5.service-go服务
service-go服务使用Go语言的不同版本分别实现了两个版本的服务,具体的代码在实验源码根目录的service/go目录下。源代码如下:
1 package main 2 3 import ( 4 "github.com/gin-gonic/gin" 5 ) 6 7 func main() { 8 r := gin.Default() 9 10 r.GET("/env", func(c *gin.Context) { 11 c.JSON(200, gin.H{ 12 "message": "go v1", 13 }) 14 }) 15 16 r.GET("/status", func(c *gin.Context) { 17 c.String(200, "ok") 18 }) 19 20 r.Run(":80") 21 }
第10~14行定义了真正的业务路由,以及返回服务的版本信息。
第16~18行定义了用于服务健康检查的路由。
第20行表示服务启动在0.0.0.0地址的80端口。
v1版本和v2版本源码只有第12行有略微差别,在v2版本中"go v1"修改为"go v2"。
用于容器化的Dockerfile文件如下:
FROM golang:1.10-alpine as builder LABEL maintainer="will835559313@163.com" COPY . /app WORKDIR /app RUN apk update && apk add git \ && go get github.com/gin-gonic/gin \ && go build FROM alpine:latest WORKDIR /app COPY --from=builder /app/app . EXPOSE 80 CMD ["./app"]
你可能已经注意到,上面的Dockerfile代码使用了两次FROM关键词和一个不太一样的COPY用法,这体现了Docker镜像的多阶段构建功能,可以在一个镜像中编译代码,然后复制编译后的产物到另一个镜像中,这样可以非常有效地减小应用的Docker镜像大小,特别是Go、Java这类编译型静态语言,因为这类语言编译之后就不再需要原来编译时的依赖库,可以把编译后的产物直接放在一个极小运行环境中启动运行。由于Go语言可以编译为在操作系统上直接运行的二进制文件,所以可以把编译后的文件直接复制到alpine这类极简的操作系统镜像中,这种优化使得service-go服务编译构建后的镜像体积可缩小到10M级别,进而使服务镜像的分发效率大幅度提升。
v1版本和v2版本的Dockerfile只有使用的基础镜像版本不同,其他保持一致。
6.service-redis服务
service-redis服务是使用Go语言实现的服务,用于从Redis服务器获取信息,具体的代码在实验源码根目录的service/redis目录下。源代码如下:
1 package main 2 3 import ( 4 "log" 5 6 "github.com/gin-gonic/gin" 7 "github.com/go-redis/redis" 8 ) 9 10 // NewClient new redis client 11 func NewClient() *redis.Client { 12 client := redis.NewClient(&redis.Options{ 13 Addr: "redis:6379", 14 Password: "", 15 DB: 0, 16 }) 17 return client 18 } 19 20 func main() { 21 r := gin.Default() 22 client := NewClient() 23 r.GET("/env", func(c *gin.Context) { 24 val, err := client.Info().Result() 25 if err != nil { 26 log.Print(err) 27 } 28 c.String(200, val) 29 }) 30 r.GET("/status", func(c *gin.Context) { 31 c.String(200, "ok") 32 }) 33 r.Run(":80") 34 }
第23~29行定义了真正的业务路由,以及返回Redis服务器的信息。
第30~32行定义了用于服务健康检查的路由。
第33行表示服务启动在0.0.0.0地址的80端口。
用于容器化的Dockerfile文件如下:
FROM golang:1.11-alpine as builder LABEL maintainer="will835559313@163.com" COPY . /app WORKDIR /app RUN apk update && apk add git \ && go get github.com/gin-gonic/gin \ && go build FROM alpine:latest WORKDIR /app COPY --from=builder /app/app . EXPOSE 80 CMD ["./app"]
7.httpbin服务
httpbin服务是一个用于HTTP测试的开源服务 。它既提供了在线的测试服务,也可以通过源码或者使用Docker镜像在本地部署运行,这两种使用方式在后续章节的实验中都有涉及。