系列目录
分布式容器集群探索—grpc服务框架envoy-grpc-web
分布式容器集群探索—Peer Discovery RabbitMQ(编写中...)
微服务容器集群探索—Consul服务注册与发现(规划中...)
服务网格探索—Envoy的xDS与ServiceMesh(规划中...)
前言
分布式,用于解决大规模计算分流问题,高峰值压力问题,服务复用问题等等。然而,在解决问题的同时,分布式更跟随带来了更多的问题,如服务管理,服务解耦,服务发现等等
所以,优秀的分布式服务框架一直是大规模后端的第一需求。当然,面向服务的框架非常多,而且大部分集中在Java领域。但是服务架构与语言分离是大方向。后端革命的浪潮中,rest和rpc是两股中坚的力量
关于rest的方案之前的文章中已经有提过不少,这一次来探索目前rpc框架中十分优秀和简洁的框架——grpc,本文结合了docker容器集群部署,给出了一套精简小巧的分布式容器集群方案x-grpc
*这一个系列的文章,全部会基于容器技术,在个人能力范围内尽可能的展示各种各样的集群解决方案,预计路径规划如下:
简单分布式集群=>
复杂分布式集群=>
混合微服务集群=>
服务网格*=>
...
当然,文章内容更多的还是展示集群雏形,在实际生产需求中,更是需要添砖加瓦,落笔记录下来自己的思考和实现过程,更主要的目的还是能够抛砖引玉,在服务架构领域,能有效参考的资料实在太少,相互交流学习是最重要的
规划
思路
- 【接口定义】grpc通过proto文件定义函数接口和消息结构
- 【服务实现】服务端选择NodeJS,通过grpc官方所建议的npm包进行proto文件加载和函数序列化
- 【服务调用】服务通过proto的加载启动后,客户端也同样根据相同的proto接口调用,实现不同主机甚至是网络节点之间的远程函数调用
- 【WEB调用】在实现了服务主机之间的相互服务调用之后,进一步的,人们自然会寄希望直接通过浏览器进行rpc服务调用,因为这可以更进一步的推进前后端的融合与标准化的工程开发。rest之所以发光发热,很大程度上也是因为其通过标准规范的推进了前后端的分离与交互。为了实现这一目标,grpc-web发布了,实现了通过envoy代理的rpc调用,虽然还不是最终完美的解决方案,但是伴随着HTTP2的推进,浏览器的更新迭代,一定在不久后的某一天,可以不需要任何代理而完成web rpc
- 【服务集群化】以上,就可以完成所有客户端服务,客户端浏览器发起rpc远程调用服务,很自然地,最后一步,服务集群化后,就可以满足超大规模rpc调用需求,自动扩容,自动负载,自动容灾等我们的核心需求。传统,完成这样一套集群,需要大规模的机房和资深的运维工程师。但是在技术领域蓬勃发展的今天,生产力极大的提高,在云服务的支撑下,利用容器技术,可以不那么费力的实现这一件目标。在这一步的思路,docker swarm是我们的主要工具
接口定义
- 登录接口
- 请求消息(帐号,密码)
- 返回消息(状态码,结果)
- 登出接口
- 请求消息(帐号)
- 返回消息(状态码,结果)
接口是解耦的核心,软件工程化的基石,有了接口,系统的实现就已经完成一半:)
服务实现
- 登录服务
- 接收帐号密码后,鉴别身份,返回登录结果
- 登出服务
- 接收帐号后,鉴别身份,返回登出结果
服务的实现不依赖语言,任何语言实现皆可
服务调用
- 通过rpc.invoke()调用函数
rpc的优雅之处在于使得集群主机的服务调用开发,和单节点的本地服务调用开发几乎没有任何区别,开发层无感知
WEB调用
- 通过envoy代理浏览器的rpc请求,打通浏览器与服务接口,浏览器使用grpc-web生成的代码进行请求
使用rpc模式的接口调用,前端人员不需要再关心网络,像调用本地函数一样调用后端接口,同时接口标准全部都以proto文件为准,不再需要原来第三方的接口管理工具例如postman等
服务集群化
- 使用docker swarm自动组网,使用overlay负载网络,全自动的容灾和部署rpc集群服务
docker swarm使得集群部署变得容易,极大的降低了集群部署门槛,虽然网络上有很多针对swarm和kubernetes的比较,国内环境很多文章会一边倒的倾向极为复杂的kubernetes,这一类的文章大多是复制转载,有些会浅浅的写一些分析,但是大部分并没有表现出对于两者的深入理解(简单的东西,就一定比不上复杂的东西?)
的确,作为Google多年来的容器编排管理工具,复杂的kubernetes有其独特的优越性,但是也不可否认docker swarm的简洁优雅。技术并不分对错,只有人分。就好比windows和linux这么多年的恩恩怨怨
方案
接口定义
user.proto
syntax = "proto3";
package demo;
// 服务定义
service User {
rpc login (LoginReq) returns (LoginRes) {}
rpc logout (LogoutReq) returns (LogoutRes) {}
}
// 请求返回定义
message LoginReq {
string username = 1;
string password = 2;
}
message LoginRes {
string code = 1;
string res = 2;
}
message LogoutReq {
string username = 1;
}
message LogoutRes {
string code = 1;
string res = 2;
}
protobuf是Google内部的混合语言数据标准,可实现跨服务,跨平台通信,因此基于grpc,分布式的所有计算节点,都只需要通过proto文件来定义接口
服务实现
user.js
// 服务实现
module.exports = {
login(call, cb) {
console.log(`${Date.now()}${JSON.stringify(call.request)}`)
cb(null, { res: `${call.request.username} 登录成功` })
},
logout(call, cb) {
console.log(`${Date.now()}${JSON.stringify(call.request)}`)
cb(null, { res: `${call.request.username} 退出成功`, code: '0' })
}
}
是的,在grpc的框架下,以上两个文件就是一个服务所必要的所有信息,理论上这两个文件就是集群服务单元:)
好吧,看到这会有很多困惑,这服务接口定义和服务编码实现都有了,可是要怎么调用呢,不同服务之间,甚至是浏览器,要怎么调用已经编码完成的
user
服务呢?所以我们需要一个小巧的grpc服务加载框架
服务加载
- protobuf的服务加载,不同语言的官方包是不一样的,在github上,我开源了一个精简小巧的nodejs加载grpc服务的框架x-grpc,在这个加载服务框架中提供以上两个写好的接口和实现文件,便可加载grpc服务
- 执行
npm install x-grpc
安装服务加载框架 - 编写
config/default.json
文件,指定服务端口以及接口文件和实现文件的根目录
{
"grpc": {
"port": 50051,
"protosDir": "/src/protos/",
"implsDir": "/src/impls/",
"serverAddress": "localhost"
}
}
- 启动
app.js
const RPCServer = require('x-grpc').RPCServer
new RPCServer(config.grpc).run()
x-grpc的加载编码在此处粘贴出来没有太大的意义,而且会显得冗余冗长,但是也不用担心,个人追求的是最少的编码,源码非常简单,对实现有兴趣的可以前往github查看源码
服务调用
client.js
是调用例子,只需要先连接服务后,调用函数请求即可完成远端服务调用
const RPCClient = require('x-grpc').RPCClient
const rpc = await new RPCClient(config.grpc).connect()
await rpc.invoke('demo.User.login', { username: 'cheney', password: '123456' })
WEB调用
- 通过protoc工具和protoc-gen-grpc-web插件,根据proto文件,生成WEB客户端pb代码
./protoc \
--plugin=./protoc-gen-grpc-web \
-I=$package $file --js_out=import_style=commonjs:$package \
--grpc-web_out=import_style=commonjs,mode=grpcwebtext:$package
$package是proto的包目录,$file是proto文件
- 引入grpc-web生成的pb文件,实例化客户端,连接远程服务envoy,调用方法即可(可使用包括vue,react在内的多种前端框架)
const { LoginReq, LoginRes } = require('./grpc/demo/user_pb.js')
const { UserClient } = require('./grpc/demo/user_grpc_web_pb.js')
let userClient = new UserClient('http://localhost:10000')
let loginReq = new LoginReq()
loginReq.setUsername('cheney')
loginReq.setPassword('123456')
userClient.login(loginReq, {}, (err, res) => {
console.error(err)
console.log(res.getRes())
})
- 访问静态资源服务
http://{staticserver}/x-grpc/web/index.html
,浏览器控制台可查看服务返回结果
服务集群化
- ssh m1 && docker swarm init --advertise-addr [m1主机的IP]
- ssh w1 && docker swarm join --token [m1的token]
- ssh m1 && docker stack deploy --prune -c docker-compose.yml [stack名称]
version: '3.7'
#服务集合
services:
#Docker集群可视化服务
portainer:
image: portainer/portainer:latest
command: ["-H", "unix:///var/run/docker.sock", "--no-auth"]
ports:
- "9090:9000"
networks:
- overlay
volumes:
- portainer:/data
- "/var/run/docker.sock:/var/run/docker.sock"
deploy:
replicas: 1
placement:
constraints: [node.role == manager]
#WEB服务代理
envoy:
image: cheney/envoy:latest
ports:
- '9091:9901'
- '10000:10000'
networks:
- overlay
deploy:
replicas: 1
placement:
constraints: [node.role == manager]
#自定义RPC服务
x-grpc:
image: cheney/x-grpc:latest
ports:
- "50051:50051"
networks:
- overlay
deploy:
replicas: 1
#卷集合
volumes:
portainer:
#网络集合
networks:
overlay:
只需要一份docker-compose.yml文件即可定义编排所需要的所有容器服务,在以上的compose文件中,所有服务都已经封装成为镜像
WEB代理—Envoy
通过官方的envoy,需要进行一些配置,根据官方文档得到的envoy的Dockerfile如下
envoy/Dockerfile
FROM envoyproxy/envoy:latest
COPY ./envoy.yaml /etc/envoy/envoy.yaml
CMD /usr/local/bin/envoy -c /etc/envoy/envoy.yaml
envoy.yaml
admin:
access_log_path: /tmp/admin_access.log
address:
socket_address: { address: 0.0.0.0, port_value: 9901 }
static_resources:
listeners:
- name: listener_0
address:
socket_address: { address: 0.0.0.0, port_value: 10000 }
filter_chains:
- filters:
- name: envoy.http_connection_manager
config:
codec_type: auto
stat_prefix: ingress_http
route_config:
name: local_route
virtual_hosts:
- name: local_service
domains: ["*"]
routes:
- match: { prefix: "/" }
route:
cluster: grpc_service
max_grpc_timeout: 0s
cors:
allow_origin:
- "*"
allow_methods: GET, PUT, DELETE, POST, OPTIONS
allow_headers: keep-alive,user-agent,cache-control,content-type,content-transfer-encoding,custom-header-1,x-accept-content-transfer-encoding,x-accept-response-streaming,x-user-agent,x-grpc-web,grpc-timeout
max_age: "1728000"
expose_headers: custom-header-1,grpc-status,grpc-message
enabled: true
http_filters:
- name: envoy.grpc_web
- name: envoy.cors
- name: envoy.router
clusters:
- name: grpc_service
connect_timeout: 0.25s
type: logical_dns
http2_protocol_options: {}
lb_policy: round_robin
hosts: [{ socket_address: { address: {stack名称}_x-grpc, port_value: 50051 }}]
通过构建以上Dockerfile就可以得到comose文件中所引用的镜像cheney/envoy
这里可以提一下envoy,这是一个现代化的面向服务的代理,功能极为强大(彻底替代nginx),围绕着它为核心,拓展出了后来的Service Mesh(服务网格)istio。这是后话了,后续的文章中会逐渐说明
envoy的配置文件稍微有一点复杂,但是简单说主要目的就是10000端口网络请求转向50051端口,而50051端口正是grpc服务所监听的端口,所以envoy在这里起到了连接web浏览器与grpc服务的作用
当使用docker stack deploy
命令一键部署完成后,我们的服务会全自动通过overlay网络,将所有镜像分发下载至所有主机,并且自动根据所配置的服务数量,自动扩容,我们可以透过一下链接访问我们所部署的服务:
访问演示
-
http://{m1domain}:9090
-
http://{m1domain}:9091
对于我们真正业务的grpc服务,我们便可以通过客户端远程调用,或web浏览器远程调用我们所部署的grpc服务集群,overlay网络会自动分流负载
后记
十分感谢您的阅读,集群系列的文章最近才开始写,中途遇到了许多困难,本文中用到的相关技术和实践,花了大约一周的时间才逐渐走通,grpc-web发布没有多久,处于实验性的完善阶段,但这是未来的一大方向。本文中所描述的分布式服务框架只是一个小小的引子,个人准备基于它拓展更多的分布式框架组件,包括服务发现集群,消息队列集群,日志处理集群,缓存集群,甚至是以后的分布式锁和分布式事务处理等等,会陆续发出文章:)
本文使用到相关技术栈官方资源集合
docker(image/compose/swarm/stack)
nodejs
envoy
grpc
grpc-web
protoc
protoc-gen-grpc-web
作者:CheneyXu
关于:Github项目源码:x-grpc