## Kubernetes、 Microservice 以及 Service Mesh 解析
文/王渊命
>虽然大家对“微服务”的定义有着不一样的看法,但对于企业业务架构微服务化趋势的看法应该没有异议的,特别是容器技术出现以后,一方面 Kubernetes 帮助微服务落地,另外一方面微服务促进了对容器和 Kubernetes 的需求,可以说微服务和 Kubernetes 相互促进,共同发展。最近又出来一个新的概念 Service Mesh,火遍容器圈,那么,Kubernetes 、 Microservice 以及 Service Mesh 这三者有着怎样的关系,如何理解三者的演进趋势呢,请看本文为你解读。
谈微服务之前,先澄清一下概念。微服务这个词的准确定义很难,不同人有不同的看法。比如一个朋友是“微服务原教旨主义者”,坚持微服务一定是无状态的 HTTP API 服务,其他都是“邪魔歪道”,它和 SOA、RPC,分布式系统之间有明显的分界。而另外也有人认为微服务本身就要求把整体系统当做一个完整的分布式应用来对待,而不是原来那种把各种组件堆积在一起,“拼接”系统的做法。两种说法都有道理,因为如果微服务没有个明确的边界,你可能会发现微服务囊括了一切,但如果只是坚持无状态,那微服务相关的一些领域又无法涵盖。我个人对这个问题持开放式的看法,微服务本身代表了一种软件交付以及复用模式的变化,从依赖库到依赖服务,和 SOA 有相通之处,同时它也带来了新的挑战,对研发运维都有影响,所有因为和微服务相关而产生的变化,都可以囊括在大微服务主题下。
### 微服务带来的变化
微服务主要给我们带来的变化有三点。
1. 部署单元的粒度越来越小,加快交付效率,同时增加运维的复杂度。
2. 依赖方式从依赖库到依赖服务,增加了开发者选择的自由(语言、框架、库),提高了复用效率,同时增加了治理的复杂度。
3. 架构模式从单体应用到微服务架构,架构设计的关注点从分层转向了服务拆分。
#### 微服务涉及的技术点
1. 服务注册与发现服务目录服务列表配置中心
2. 进程间通讯负载均衡
3. 服务生命周期管理部署、变更、升级、自动化运维
4. 服务依赖关系
5. 链路跟踪、限流、降级、熔断
6. 访问控制
7. 日志与监控
8. 微服务应用框架
这个是我浏览了众多微服务的话题之后摘要出来的一些技术点,不全面也不权威,不过可以看出,微服务主题涉及的面非常广,那这些问题在微服务之前不存在么?为什么大家谈到微服务的时候才把这些东西拿出来说。这需要从软件开发的历史说来。软件开发行业从十多年前开始,出现了一个分流,一部分是企业应用开发,软件要安装到企业客户自己的资源上,客户负责运维(或者通过技术支持),一部分是互联网软件,自己开发运维一套软件通过网络给最终用户使用。这两个领域使用的技术栈也逐渐分化,前者主要关注标准化、框架化(复用)、易安装、易运维、后者主要关注高可用、高性能、纵向伸缩。而这两个领域到微服务时代,形成了一个合流,都在搞微服务化。主要原因我认为有两点:
1. SaaS 的兴起,使得一些企业应用厂商也开始采用互联网模式,遇到用户规模的问题。同时企业应用很难纯 SaaS 化,面对大客户的时候,势必面临私有部署的问题,所以必须探索一种既能支撑用户规模,同时要能方便私有化部署的架构。
2. 随着互联网技术领域为了应对新的业务变化,需要将原来的技术经验沉淀继承过来,开始关注标准化和框架化。
也就是说,这些技术点并不是新问题,但如何将这些技术点从业务逻辑中抽取出来,作为独立的,可复用的框架或者服务,这个是大家探寻的新问题。
### 为微服务而生的 Kubernetes
1. Kubernetes Pod - Sidecar 模式
2. Kubernetes 支持微服务的一些特性
3. Service Mesh 微服务的中间件层
微服务和 Kubernetes 确实是相辅相成的。一方面
Kubernetes 帮助微服务落地,另外一方面微服务促进了对容器和 Kubernetes 的需求。
### Kubernetes 的 Pod - Sidecar 模式
我们先从 Kubernetes 的 Pod 机制带来的一种架构模式 —Sidecar 来说。下面这段配置文件是我从 Kubernetes 内置的 dns 的 deployment 中抽取出来的 Pod 描述文件,简化掉了资源限制,端口设置以及健康检查等内容。
```
apiVersion: v1
kind: Pod
metadata:
name: dnspod
spec:
containers:
- name: kubedns
image: gcr.io/google_containers/k8s-dns-kube-dns-amd64:1.14.4
args:
- --domain=cluster.local.
- --dns-port=10053
- --config-dir=/kube-dns-config
- --v=2
ports:
- containerPort: 10053
name: dns-local
protocol: UDP
- containerPort: 10053
name: dns-tcp-local
protocol: TCP
- name: dnsmasq
image: gcr.io/google_containers/k8s-dns-dnsmasq-nanny-amd64:1.14.4
args:
- -v=2
- -logtostderr
- -configDir=/etc/k8s/dns/dnsmasq-nanny
- -restartDnsmasq=true
- --
- -k
- --cache-size=1000
- --log-facility=-
- --server=/cluster.local/127.0.0.1#10053
- --server=/in-addr.arpa/127.0.0.1#10053
- --server=/ip6.arpa/127.0.0.1#10053
ports:
- containerPort: 53
name: dns
protocol: UDP
- containerPort: 53
name: dns-tcp
protocol: TCP
- name: sidecar
image: gcr.io/google_containers/k8s-dns-sidecar-amd64:1.14.4
args:
- --v=2
- --logtostderr
- --probe=kubedns,127.0.0.1:10053,kubernetes.default.svc.cluster.local,5,A
- --probe=dnsmasq,127.0.0.1:53,kubernetes.default.svc.cluster.local,5,A
```
从这个配置文件可以看出,Kubernetes 的 Pod 里可以包含多个容器,同一个 Pod 的多个容器之间共享网络,Volume,IPC(需要设置),生命周期和 Pod 保持一致。上例中的 kube-dns 的作用是通过 Kubernetes 的 API 监听集群中的 Service 和 Endpoint 的变化,生成 dns 记录,通过 dns 协议暴露出来。dnsmasq 的作用是作为dns缓存,cluster 内部域名解析代理到 kube-dns,其他域名通过上游 dns server 解析然后缓存,供集群中的应用使用。sidecar 容器的作用是进行 dns 解析探测,然后输出监控数据。
通过这个例子可以看出来,Kubernetes 的 Pod 机制给我们提供了一种能力,就是将一个本来要捆绑在一起的服务,拆成多个,分为主容器和副容器(sidecar),是一种更细粒度的服务拆分能力。当然,没有 Kubernetes 的时候你也可以这么做,但如果程序需要几个进程捆绑在一起,要一起部署迁移,运维肯定想来打你。有了这种能力,我们就可以用一种非侵入的方式来扩展服务能力,并且几乎没有增加运维复杂度。这个在后面的例子中也可以看到。
### Kubernetes 上的服务发现
服务发现其实包含了两个方面的内容,一种是要发现应用依赖的服务,这个 Kubernetes 提供了内置的 dns 和 ClusterIP 机制,每个 Service 都自动注册域名,分配 ClusterIP,这样服务间的依赖可以从 IP 变为 name,这样可以实现不同环境下的配置的一致性。
```
apiVersion: v1
kind: Service
metadata:
name: myservice
spec:
ports:
- port: 80
protocol: TCP
targetPort: 9376
selector:
app: my-app
clusterIP: 10.96.0.11
curl http://myservice
```
如上面的 Service 的例子,依赖方只需要通过 myservice 这个名字调用,具体这个 Service 后面的后端实例在哪,有多少个,用户不用关心,交由
Kubernetes 接管,相当于一种基于虚 IP(通过 iptables 实现)的内部负载均衡器。(具体的 clusterIP 实现这里不再详述,可查阅相关资料)。
另外一种服务发现是需要知道同一个服务的其他容器实例,节点之间需要互相连接,比如一些分布式应用。这种需求可以通过 Kubernetes 的 API 来实现,比如以下实例代码:
```
endpoints, _ = client.Core().Endpoints(namespace).Get("myservice", metav1.GetOptions{})
addrs := []string{}
for _, ss := range endpoints {
for _, addr := range ss.Addresses {
ips = append(addrs, fmt.Sprintf(`"%s"`, addr.IP))
}
}
glog.Infof("Endpoints = %s", addrs)
```
Kubernetes 提供了 ServiceAccount 的机制,自动在容器中注入调用 Kubernetes API 需要的 token,应用代码中无需关心认证问题,只需要部署的时候在 yaml 中配置好合适的 ServiceAccount 即可。
关于服务发现再多说两句,没有 Kubernetes 这样的统一平台之前,大家做服务发现还主要依赖一些服务发现开源工具,比如 Etcd,ZooKeeper,Consul 等,进行自定义开发注册规范,在应用中通过 SDK 自己注册。但当应用部署到 Kubernetes 这样的平台上你会发现,应用完全不需要自己注册,Kubernetes 本身最清楚应用的节点状态,有需要直接通过 Kubernetes 进行查询即可,这样可以降低应用的开发运维成本。
### 通过 Kubernetes 进行 Leader 选举
有些分布式需要 Leader 选举,在之前,大家一般可能依赖 Etcd 或者 ZooKeeper 这样的服务来实现。如果应用部署在 Kubernetes 中,也可以利用 Kubernetes 来实现选举,这样可以减少依赖服务。
Kubernetes 中实现 Leader 选举主要依赖以下特性:
1. Kubernetes 中的所有 API 对象类型,都有一个
ResourceVersion,每次变更,版本号都会增加。
2. Kubernetes 中的所有对象,都支持 Annotation,支持通过 API 修改,这样可以附加一些自定义的 key-value 数据保存到 Kubernetes 中。
3. Kubernetes 的所有 API 对象中,Endpoint/ConfigMap 属于“无副作用”对象,也就是说,创建后不会带来额外的影响,所以一般用这两种对象来保存选主信息。
4. Kubernetes 的 Update/Replace 接口,支持 CAS 机制的更新,也就是说,更新时可以带上客户端缓存中的 ResourceVersion,如果服务器端该对象的 ResourceVersion 已经大于客户端传递的 ResourceVersion,则会更新失败。
这样,所有的节点都一起竞争更新同一个 Endpoint/ConfigMap,更新成功的,作为 Leader,然后把 Leader 信息写到 Annotation 中,其他节点也能获取到。为了避免竞争过于激烈,会有一个过期机制,过期时间也写入到 Annotation,Leader 定时 renew 过期时间,其他节点定时查询,发现过期就发起新一轮的竞争。这样相当于 Kubernetes 提供了一种以 client 和 API 一起配合实现的 Leader 选举机制,并且在 client sdk 中提供。当前 Kubernetes 的一些内部组件,比如 controller-manager,也是通过这种方式来实现选举的。
当然,如果觉得调用 client 麻烦,或者该语言的 SDK 尚未支持这个特性(应该只有 Go 支持了),可以也可以通过 sidercar 的方式实现,参看 https://github.com/kubernetes/contrib/tree/master/election。也就是说,通过一个 sidecar 程序去做选主,主程序只需要调用 sidecar 的一个 HTTP API 去查询即可。
```
$ kubectl exec elector-sidecar -c nodejs -- wget -qO- http://localhost:8080
Master is elector-sidecar
```
### Kubernetes 支持微服务的运维特性
Kubernetes 对微服务的运维特性上的支持,主要体现以下两方面:
#### 滚动升级以及自动化伸缩
```
kubectl set image deployment =
kubectl rollout status deployment
kubectl rollout pause deployment
kubectl rollout resume deployment
kubectl rollout undo deployment
kubectl autoscale deployment php-apache --cpu-percent=50 --min=1 --max=10
```
滚动升级支持最大不可用节点设置,支持暂停,恢复,一键回滚。自动伸缩支持根据监控数据在一个范围内自动伸缩,很大程度上降低了微服务的运维成本。
#### 日志与监控的标准化
通过 Kubernetes 可以实现日志收集以及应用监控的标准化、自动化,这样应用不用关心日志和监控数据的收集展示,只需要按照系统标准输出日志和监控接口即可。
### Service Mesh 微服务的中间件层
微服务这个话题虽然火了很长时间,关于微服务的各种框架也有一些。但这些框架大多是编程语言层面来解决,需要用户业务代码中集成框架的类库,语言的选择也受限。这种方案很难作为单独的产品或者服务给用户使用,升级更新也受限于应用本身的更新与迭代。直到 Service Mesh 的概念的提出。Service Mesh 貌似也没有比较契合的翻译(有的译做服务齿合层,有的翻译做服务网格),这个概念就是试图在网络层抽象出一层,来统一接管一些微服务治理的功能。这样就可以做到跨语言、无侵入、独立升级。其中前一段时间 Google、IBM、Lyft 联合开源的 istio 就是这样一个工具,先看下它的功能简介:
1. 智能路由以及负载均衡
2. 跨语言以及平台
3. 全范围(Fleet-wide)策略执行
4. 深度监控和报告
是不是听上去就很厉害?有的还搞不明白是啥意思?我们看 istio 之前先看看 Service Mesh 能在网络层做些什么。
1. 可视化其实本质上微服务治理的许多技术点都包含可视化要求,比如监控和链路追踪,比如服务依赖
2. 弹性(Resiliency 或者应该叫柔性,因为弹性很容易想到 scale)就是网络层可以不那么生硬,比如超时控制、重试策略、错误注入、熔断、延迟注入都属于这个范围。
3. 效率(Efficiency)网络层可以帮应用层多做一些事情,提升效率。比如卸载 TLS,协议转换兼容。
4. 流量控制比如根据一定规则分发流量到不同的 Service 后端,但对调用方来说是透明的。
5. 安全保护在网络层对流量加密/解密,增加安全认证机制,而对应用透明。
可以看出,如果接管了应用的进出流量,将网络功能可编程化,实际上可做的事情很多。那我们简单看下 istio 是如何做的。
大致看一下图1,istio 在业务 Pod 里部署了一个 sidecar—Envoy,这是一个代理服务器,前面说的网络层功能基本靠它来实现,然后 Envoy 和上面的控制层组件(Mixer、Pilot、Istio-Auth)交互,实现动态配置,策略执行,安全证书获取等,对用户业务透明。实际部署的时候,并不需要开发者在自己的 Pod 声明文件里配置 Envoy 这个 sidecar,istio 提供了一个命令行工具,在部署前注入(解析声明配置文件然后自动修改)即可,这样 istio 的组件就可以和业务应用完全解耦,进行独立升级。
图1 istio 架构图
看到这里,Java 服务器端的研发人员可能会感觉到,这个思路和 Java 当初的 AOP(aspect-oriented programming)有点像,确实很多 Java 微服务框架也是利用 AOP 的能力来尽量减少对应用代码的侵入。但程序语言的 AOP 能力受语言限制,有的语言里就非常难实现非侵入的 AOP,而如果直接在网络层面寻找切面就可以做到跨语言了。
当前其实已经有许多在网络层实现的中间件,比如有提供数据库安全审计功能的,有提供 APM 的,有提供数据库自动缓存的,但这些中间件遇到的最大问题是增加了用户应用的部署复杂度,实施成本比较高,很难做到对用户应用透明。可以预见,随着 Kubernetes 的普及,这类中间件也会涌现出来。
### 结语
本质上,微服务的目的是想以一种架构模式,应对软件所服务的用户的规模增长。没有微服务架构之前,大多数应用是以单体模式出现的,只有当规模增长到一定程度,单体架构满足不了伸缩的需求的时候,才考虑拆分。而微服务的目标是在一开始就按照这种架构实现,是一种面向未来的架构,也就是说用开始的选择成本降低以后的重构成本。用经济学的观点来说,微服务是技术投资,单体应用是技术债务,技术有余力那可以投资以期待未来收益,没余力那就只能借债支持当前业务,等待未来还债。而随着微服务基础设施的越来越完善,用很小的投资就可以获得未来很大的收益,就没有理由拒绝微服务了。