现在,我们开始为服务网格编写微服务应用了。
我们以一个电商购物网站为例来说明。
腾讯网格商店介绍
腾讯网格商店(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: