使用服务网格/Istio开发微服务2:应用开发

2020-05-25 10:19:23 浏览数 (1)

现在,我们开始为服务网格编写微服务应用了。

我们以一个电商购物网站为例来说明。

腾讯网格商店介绍

腾讯网格商店(Tencent Mesh Shop) 【账号:demo/111111】是一个购物网站示例。他使用服务网格技术进行部署,使用了多种编程语言,包括 java,go, nodejs, python, c# 等。

各个模块说明:

文件目录

开发语言

说明

/apps/passport

Nodejs

账号系统

/apps/product

Java

商品服务

/apps/promotion

Python

促销服务

/apps/stock

Python

库存服务

/apps/review

Java

评论服务

/apps/mall

react nodejs

商城前端

/apps/shopcart

go

购物车服务

/apps/order

C# .net core

订单服务

/deploy/mesh

YAML

Istio 部署脚本

调用链跟踪调用链跟踪
腾讯云后台自动生成的网络拓扑腾讯云后台自动生成的网络拓扑

Istio 对业务代码的“侵入点”

虽然为服务网格编写应用号称是“无入侵”,但下面的两点还是有一点变化。

远程调用路径

在服务网格中,使用内部 DNS 技术,将服务名/域名映射成为了 ip 地址,所以,一般的调用方式是服务名 端口。如下的路径在服务网格中都被支持。

  • <服务名>:<端口>
  • <服务名>.<命名空间>:<端口>
  • <服务名>.<命名空间>.<DNS限定名>:<端口>

一个完整的域名如下:

http://passport.xyz.svc.cluster.local:7301

流量如果要被治理,那么在应用中需要使用服务名来调用服务。

在程序中硬编码建议写成 服务名 调用:封装成统一的方法。把真实的 服务名/域名 和 端口写入配置文件进行程序外加载。

调用链跟踪

为了让服务网格能追踪你的调用链,你必须在远程调用的时候传递如下的 header:

代码语言:txt复制
[
   "x-request-id",
   "x-b3-traceid",
   "x-b3-spanid",
   "x-b3-parentspanid",
   "x-b3-sampled",
   "x-b3-flags",
   "x-ot-span-context",
   "x-cloud-trace-context",
   "traceparent",
   "grpc-trace-bin"
]

你无需去给这些参数手动赋值,istio 会在第一次发起调用的时候管理这些参数,你只要在调用链中透传就好。

但在流量治理的时候,有些策略是按照 header 里面的 自定义的 tag 分配,所以我们不应固化这些 tag,因为我们并不知道自定义的 tag 在哪个环节添加的。一般情况下,针对某一语言,编写统一的远程调用方法。

下面一个 go echo 的封装示例,这个方法中传递了所有上下文的 header 参数 :

代码语言:txt复制
// Fetch godoc
// 通用远程调用方法
func Fetch(ctx echo.Context, method, url string, body io.Reader) (string, error) {
	headers := ctx.Request().Header
	client := &http.Client{}
	//提交请求
	reqest, err := http.NewRequest(method, url, body)
	if err != nil {
		return "", err
	}
	//pass through headers
	for k, v := range headers {
		if v != nil && len(v) > 0 {
			reqest.Header.Add(k, v[0])
		}
	}
	//处理返回结果
	response, err := client.Do(reqest)
	if err != nil {
		return "", err
	}
	defer response.Body.Close()

	bytes, err := ioutil.ReadAll(response.Body)
	if err != nil {
		return "", err
	}

	strBody := string(bytes[:])

	return strBody, nil
}

准备部署

TKE Mesh 环境准备

首先,在腾讯云后台部署 TKE Mesh 环境(目前处于白名单开放状态)。

进入腾讯云控制台。

1、容器服务->集群->新建。稍等,等他完成。建议使用托管集群,初始开通的服务器要大于3台(mesh 会消耗一些资源)。

购买容器服务购买容器服务

2、容器服务->服务网格 -> 新建。这个步骤将会把服务网格安装到刚刚新建的 TKE 容器集群。

开通服务网格开通服务网格

按照文档说明,将 TKE 的秘钥安装到本地,使得可以通过本地的 kubectl/istioctl 访问远程集群。

应用打包 Dockerfile

docker 打包文件一般都是开发者来编写,可以使用运维提供的统一模板。

一般有如下的准则:

  • 不为某个特定的环境打包
  • 使用最小镜像

如这个 nodejs 的 Dockerfile,使用了 alpine 的镜像,没有编写 ENTRYPOINT,启动脚本将在编排脚本中编写。

代码语言:txt复制
FROM node:13-alpine
RUN mkdir /app
COPY . /app
WORKDIR /app
RUN npm i --production
EXPOSE 7301

k8s 部署脚本中启动:

代码语言:txt复制
#...
      containers:
      - image: ccr.ccs.tencentyun.com/arche-cloud/passport:1.0.5
        imagePullPolicy: IfNotPresent
        name: passport
        env:
        - name: EGG_SERVER_ENV
          value: prod
        workingDir: /app
        args:
        - node
        - index.js
#...

而这个 springboot 的 Dockerfile,虽然我们指定了ENTRYPOINT:

代码语言:txt复制
FROM openjdk:15-alpine
RUN mkdir /app
COPY target/review.jar /app
WORKDIR /app
EXPOSE 8004
ENTRYPOINT ["java","-jar","review.jar"]

但在 k8s 的部署脚本中,可以继续指定容器的启动参数:

代码语言:txt复制
#...
      containers:
      - name: xyzdemo-review
        image: ccr.ccs.tencentyun.com/axlyzhang-images/xyzdemo-review:v1.10
        args: ["--spring.profiles.active=prod", "--spring.config.location=application-prod.yml"]
#...

最终运行的脚本变为:

代码语言:txt复制
java -jar review.jar --spring.profiles.active=prod --spring.config.location=application-prod.yml

部署服务

1、首先创建一个新的 namespace,打开自动 sidecar 注入。建议使用 namespace 来隔离区分一组应用。

代码语言:txt复制
apiVersion: v1
kind: Namespace
metadata:
  name: xyz
  labels:
    istio-injection: enabled
spec:
  finalizers:
    - kubernetes

2、首先我们将生产环境的配置文件写到 ConfigMap。

下面的例子就是把 springboot 的 application.yml 文件直接写入 ConfigMap 了,在部署的时候,我们将会把这个配置映射为容器的文件。

代码语言:txt复制
apiVersion: v1
kind: ConfigMap
metadata:
  name: xyz-demo
  namespace: xyz
data: 
  review-config: |-
    server:
      port: 8004
    spring:
      application:
        name: xyzdemo-review
      datasource:
        url: jdbc:mysql://10.0.30.18/review?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC&useSSL=true
        username: root
        password: xxxxxx
        driver-class-name: com.mysql.cj.jdbc.Driver
    pagehelper:
      helperDialect: mysql
      reasonable: true
      support-methods-arguments: true
      params: count=countSql

3、部署应用:

代码语言:txt复制
apiVersion: apps/v1
kind: Deployment
metadata:
  name: xyzdemo-review
  namespace: xyz
  labels:
    app: xyzdemo-review
    version: v1
spec:
  replicas: 4 # 副本数量
  selector:
    matchLabels:
      app: xyzdemo-review
      version: v1
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: xyzdemo-review
        version: v1
    spec:
      containers:
      - name: xyzdemo-review
        image: ccr.ccs.tencentyun.com/axlyzhang-images/xyzdemo-review:v1.10
        args: ["--spring.profiles.active=prod", "--spring.config.location=application-prod.yml"]
        ports:
        - containerPort: 8004
          protocol: TCP
        volumeMounts:  # 指定一个文件绑定,目标路径是容器的 /app/application-prod.yml
        - name: prod-config
          mountPath: /app/application-prod.yml 
          subPath: application-prod.yml  # 当前目录下如果有其他文件,必须指定 subPath,否则 app 目录下的所有文件都会被覆盖。
        resources:
          limits:
            cpu: 500m
            memory: 1Gi
          requests:
            cpu: 250m
            memory: 256Mi
        securityContext:
          privileged: false
          procMount: Default
      volumes:
      - name: prod-config
        configMap: # 这里对应 ConfigMap 里的配置
          name: xyz-demo 
          items:
          - key: review-config
            path: application-prod.yml 
      imagePullSecrets: # 这个 key 是你创建的针对加密的容器镜像地址的登录凭证。
        - name: qcloudregistrykey

创建加密的镜像地址登录 key

代码语言:txt复制
kubectl create secret docker-registry qcloudregistrykey --docker-server=<your-registry-server> --docker-username=<your-name> --docker-password=<your-pword> --docker-email=<your-email>

4、 为你的部署创建一个服务

代码语言:txt复制
apiVersion: v1
kind: Service
metadata:
  name: xyzdemo-review-service
  namespace: xyz
spec:
  type: ClusterIP
  ports:
    - name: http
      port: 8004
  selector:
    app: xyzdemo-review

5 创建 egress,使得服务能访问外部的云数据库。

代码语言:txt复制
# 开放集群对云数据库的访问
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: mysql-external
spec:
  hosts:
  - mysql-host
  addresses:
  - 10.0.30.18
  ports:
  - name: tcp
    number: 3306
    protocol: tcp
  location: MESH_EXTERNAL

部署前端应用

xyzdemo 在开发过程中使用了 react,是一个前后端分离的单页应用。前端是一个纯的静态页面应用。

当然,发布静态页面应用,你也可以使用上文所述的服务部署方式整体发布打包部署。

我们这里前端应用部署了一个 nginx 作为服务和页面的代理。mall 应用本身没有 Dockerfile,而是直接将 build 的结果上传到了 cos。

部署静态页面

1、Build 前端应用,为了使用 CDN 的能力我们在 build react 的时候,在 package.json 中使用了如下脚本:

代码语言:txt复制
  "scripts": {
    "build": "GENERATE_SOURCEMAP=false PUBLIC_URL=https://xyz-mesh-mall-1258272208.cos-website.ap-guangzhou.myzijiebao.com react-app-rewired build",
  }

PUBLIC_URL 是 cos 的对外服务地址,在实际使用中,建议映射成一个 CDN 的域名地址。

2、编写 nginx 配置文件,存储到 ConfigMap

代码语言:txt复制
apiVersion: v1
kind: ConfigMap
metadata:
  name: xyz-demo
  namespace: xyz
data: 
  mall-config: |-
    server { 
      listen       8080;
      index   index.html;
      location / {
        proxy_http_version 1.1;
        proxy_pass  https://xyz-mesh-mall-1258272208.cos-website.ap-guangzhou.myzijiebao.com;
        rewrite ^/(.*)$ /index.html break;  # 单页应用,不使用 hash 路径,会将所有的未知地址映射到 index.html
      }

      location ^~/api/passport/ {
        proxy_http_version 1.1;
        proxy_pass      http://passport:7301/;
      }
      location ^~/api/product/ {
        proxy_http_version 1.1;
        proxy_pass      http://xyzdemo-product-service:8003/;
      }
      location ^~/api/review/ {
        proxy_http_version 1.1;
        proxy_pass      http://xyzdemo-review-service:8004/;
      }
      location ^~/api/shopcart/ {
        proxy_http_version 1.1;
        proxy_pass      http://shopcart:7311/;
      }
      location ^~/api/order/ {
        proxy_http_version 1.1;
        proxy_pass      http://order:7312/;
      }
    }

3、部署应用,实际上我们部署的只是一个 nginx 而已。

代码语言:txt复制
apiVersion: apps/v1
kind: Deployment
metadata:
  name: mall-v1
  namespace: xyz
  labels: 
    app: mall
    version: v1
spec:
  replicas: 1
  selector:
    matchLabels:
      app: mall
      version: v1
  template:
    metadata:
      labels:
        app: mall
        version: v1
    spec:
      containers:
      - image: nginx:alpine
        imagePullPolicy: IfNotPresent
        name: mall
        ports:
        - containerPort: 8080
          protocol: TCP
        volumeMounts:
        - name: prod-config
          mountPath: /etc/nginx/conf.d/
      volumes:
      - name: prod-config
        configMap:
          name: xyz-demo
          items:
          - key: mall-config
            path: mall.conf

4、为这个 Deployment 创建一个 Service。和上面的差不多,略...。

5、egress 开放访问 COS

代码语言:txt复制
apiVersion: networking.istio.io/v1alpha3
kind: ServiceEntry
metadata:
  name: cdn-external
spec:
  hosts:
  - xyz-mesh-mall-1258272208.cos-website.ap-guangzhou.myzijiebao.com
  ports:
  - number: 443
    name: https
    # protocol: HTTPS
  resolution: DNS
  location: MESH_EXTERNAL

6、创建一个 ingress-gateway 和 VirtualService 来开放 mall 应用。

代码语言:txt复制
apiVersion: networking.istio.io/v1alpha3
kind: Gateway
metadata:
  name: mall-tmp-gateway
  namespace: xyz
spec:
  selector:
    istio: ingressgateway # use istio default controller
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "*"
---
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
  name: mall-vs
  namespace: xyz
spec:
  hosts:
  - "*"
  gateways:
  - mall-tmp-gateway
  http:
  - match:
    - uri:
        prefix: /
    route:
    - destination:
        host: mall
        port:
          number: 8080

7、绑定域名....

当所有的服务都部署完成,并调通了之后,我们的应用就run起来了。

调试应用的一些有用命令

  • 部署失败使用 kubectl describe 查看失败的 pod 信息:kubectl describe po -n <namespace> <pod-name>
  • 部署失败使用 kubectl logs 查看日志kubectl logs -n <namespace> -p <pod-name>
  • pod部署成功,程序运行失败,进入容器内部找原因:kubectl exec -it -n <namespace> <pod-name> -- sh进入容器内部,看看配置文件内容,环境变量,wget/curl 一下自己或内部服务等等...
  • 对于多集群,要在集群间切换配置文件,我配置了几个 alias 在 .zshrc 里,如下:alias kbgz="kubectl --kubeconfig ~/.kube/cluster-gz.yaml" alias kbbj="kubectl --kubeconfig ~/.kube/cluster-bj.yaml"使用不同的命令就可以连接不同的集群了。

总结

在编写和部署服务网格应用过程中,我们并未使用任何框架,没有在应用中编写任何“服务治理" 的代码,但我们的应用却具有了“微服务”的能力。

编写应用变得简单了,但部署变复杂了,好在我们的部署过程都记录在案(GitOps)。我们还有 CI/CD 来辅助,一切都来得那么顺其自然。

这大概就是 Service Mesh /云原生 的精妙之处吧。:smiley_cat:

0 人点赞