前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使用服务网格/Istio开发微服务2:应用开发

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

原创
作者头像
谢正伟
修改2020-05-25 10:19:23
1.6K0
修改2020-05-25 10:19:23
举报
文章被收录于专栏:云原生研究

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

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

腾讯网格商店介绍

腾讯网格商店(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.myqcloud.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.myqcloud.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.myqcloud.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:

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 腾讯网格商店介绍
  • Istio 对业务代码的“侵入点”
    • 远程调用路径
      • 调用链跟踪
      • 准备部署
        • TKE Mesh 环境准备
          • 应用打包 Dockerfile
            • 部署服务
              • 部署前端应用
                • 部署静态页面
            • 调试应用的一些有用命令
            • 总结
            相关产品与服务
            容器服务
            腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档