作者 | 李志信
dubbo-go 源码 :https://github.com/apache/dubbo-go
导读 :随着微服务架构的风行,许多高性能 rpc 框架应运而生,由阿里开源的 dubbo 框架 go 语言版本的 dubbo-go 也成为了泛滥开发者不错的抉择。本文将介绍 dubbo-go 框架的根本应用办法,以及从 export 调用链的角度进行 server 端源码导读,心愿能疏导读者进一步意识这款框架。下周将发表本文的姊妹篇:《从 client 端源码导读 dubbo-go 框架》。
当拿到一款框架之后,一种不错的源码浏览形式大抵如下:从运行最根底的 helloworld demo 源码开始 —> 再查看配置文件 —> 开启各种依赖服务(比方 zk、consul)—> 开启服务端 —> 再到通过 client 调用服务端 —> 打印残缺申请日志和回包。调用胜利之后,再依据框架的设计模型,从配置文件解析开始,自顶向下递浏览整个框架的调用栈。
对于 C/S 模式的 rpc 申请来说,整个调用栈被拆成了 client 和 server 两局部,所以能够别离从 server 端的配置文件解析浏览到 server 端的监听启动,从 client 端的配置文件解析浏览到一次 invoker Call 调用。这样一次残缺申请就清晰了起来。
运行官网提供的 helloworld-demo
官网 demo 相干链接 :https://github.com/dubbogo/dubbo-samples/tree/master/golang/helloworld/dubbo
1. dubbo-go 2.7 版本 QuickStart
1)开启一个 go-server 服务
- 将仓库 clone 到本地
$ git clone https://github.com/dubbogo/dubbo-samples.git
- 进入 dubbo 目录
$ cd dubbo-samples/golang/helloworld/dubbo
进入目录后可看到四个文件夹,别离反对 go 和 java 的 client 以及 server,咱们尝试运行一个 go 的 server。进入 app 子文件夹内,能够看到外面保留了 go 文件。
$ cd go-server/app
- sample 文件构造
能够在 go-server 外面看到三个文件夹:app、assembly、profiles。
其中 app 文件夹下保留 go 源码,assembly 文件夹下保留可选的针对特定环境的 build 脚本,profiles 下保留配置文件。对于 dubbo-go 框架,配置文件十分重要,没有文件将导致服务无奈启动。
- 设置指向配置文件的环境变量
因为 dubbo-go 框架依赖配置文件启动,让框架定位到配置文件的形式就是通过环境变量来找。对于 server 端须要两个必须配置的环境变量:CONF_PROVIDER_FILE_PATH、APP_LOG_CONF_FILE,别离应该指向服务端配置文件、日志配置文件。
在 sample 外面,咱们能够应用 dev 环境,即 profiles/dev/log.yml 和 profiles/dev/server.yml 两个文件。在 app/ 下,通过命令行中指定好这两个文件:
$ export CONF_PROVIDER_FILE_PATH=”../profiles/dev/server.yml”
$ export APP_LOG_CONF_FILE=”../profiles/dev/log.yml”
- 设置 go 代理并运行服务
$ go run .
如果提醒 timeout,则须要设置 goproxy 代理。
$ export GOPROXY=”http://goproxy.io”
再运行 go run 即可开启服务。
2)运行 zookeeper
装置 zookeeper,并运行 zkServer, 默认为 2181 端口。
3)运行 go-client 调用 server 服务
- 进入 go-client 的源码目录
$ cd go-client/app
- 同理,在 /app 下配置环境变量
$ export CONF_CONSUMER_FILE_PATH=”../profiles/dev/client.yml”
$ export APP_LOG_CONF_FILE=”../profiles/dev/log.yml”
配置 go 代理:
$ export GOPROXY=”http://goproxy.io”
- 运行程序
$ go run .
即可在日志中找到打印出的申请后果:
response result: &{A001 Alex Stocks 18 2020-10-28 14:52:49.131 +0800 CST}
同样,在运行的 server 中,也能够在日志中找到打印出的申请:
req:[]interface {}{“A001”}
rsp:main.User{Id:”A001″, Name:”Alex Stocks”, Age:18, Time:time.Time{…}
祝贺!一次基于 dubbo-go 的 rpc 调用胜利。
4)常见问题
- 当日志开始局部呈现 profiderInit 和 ConsumerInit 均失败的日志,查看环境变量中配置门路是否正确,配置文件是否正确。
- 当日志中呈现 register 失败的状况,个别为向注册核心注册失败,查看注册核心是否开启,查看配置文件中对于 register 的端口是否正确。
- sample 的默认开启端口为 20000,确保启动前无占用。
2. 配置环境变量
export APP_LOG_CONF_FILE="../profiles/dev/log.yml"
export CONF_CONSUMER_FILE_PATH="../profiles/dev/client.yml"
3. 服务端源码
1)目录构造
dubbo-go 框架的 example 提供的目录如下:
- app/ 文件夹下寄存源码,能够本人编写环境变量配置脚本 buliddev.sh
- assembly/ 文件夹下寄存不同平台的构建脚本
- profiles/ 文件夹下寄存不同环境的配置文件
- target/ 文件夹下寄存可执行文件
2)要害源码
源码搁置在 app/ 文件夹下,次要蕴含 server.go 和 user.go 两个文件,顾名思义,server.go 用于应用框架开启服务以及注册传输协定;user.go 则定义了 rpc-service 构造体,以及传输协定的构造。
- user.go
func init() {config.SetProviderService(new(UserProvider))
// ------for hessian2------
hessian.RegisterPOJO(&User{})
}
type User struct {
Id string
Name string
Age int32
Time time.Time
}
type UserProvider struct {
}
func (u *UserProvider) GetUser(ctx context.Context, req []interface{}) (*User, error) {
能够看到,user.go 中存在 init 函数,是服务端代码中最先被执行的局部。User 为用户自定义的传输构造体,UserProvider 为用户自定义的 rpc_service;蕴含一个 rpc 函数,GetUser。当然,用户能够自定义其余的 rpc 性能函数。
在 init 函数中,调用 config 的 SetProviderService 函数,将以后 rpc_service 注册在框架 config 上。
能够查看 dubbo 官网文档提供的设计图:
service 层上面就是 config 层,用户服务会逐层向下注册,最终实现服务端的裸露。
rpc-service 注册结束之后,调用 hessian 接口注册传输构造体 User。
至此,init 函数执行结束。
- server.go
// they are necessary:
// export CONF_PROVIDER_FILE_PATH="xxx"
// export APP_LOG_CONF_FILE="xxx"
func main() {hessian.RegisterPOJO(&User{})
config.Load()
initSignal()}
func initSignal() {signals := make(chan os.Signal, 1)
...
之后执行 main 函数。
main 函数中只进行了两个操作,首先应用 hessian 注册组件将 User 构造体注册(与之前略有反复),从而能够在接下来应用 getty 打解包。
之后调用 config.Load 函数,该函数位于框架 config/config_loader.go 内,这个函数是整个框架服务的启动点, 上面会具体讲这个函数内重要的配置处理过程 。执行完 Load() 函数之后,配置文件会读入框架,之后依据配置文件的内容,将注册的 service 实现到配置构造里,再调用 Export 裸露给特定的 registry,进而开启特定的 service 进行对应端口的 tcp 监听,胜利启动并且裸露服务。
最终开启信号监听 initSignal() 优雅地完结一个服务的启动过程。
4. 客户端源码
客户端蕴含 client.go 和 user.go 两个文件,其中 user.go 与服务端完全一致,不再赘述。
- client.go
// they are necessary:
// export CONF_CONSUMER_FILE_PATH="xxx"
// export APP_LOG_CONF_FILE="xxx"
func main() {hessian.RegisterPOJO(&User{})
config.Load()
time.Sleep(3e9)
println("\n\n\nstart to test dubbo")
user := &User{}
err := userProvider.GetUser(context.TODO(), []interface{}{"A001"}, user)
if err != nil {panic(err)
}
println("response result: %v\n", user)
initSignal()}
main 函数和服务端也相似,首先将传输构造注册到 hessian 上,再调用 config.Load() 函数。在下文会介绍,客户端和服务端会依据配置类型执行 config.Load() 中特定的函数 loadConsumerConfig() 和 loadProviderConfig(),从而达到“开启服务”、“调用服务”的目标。
加载完配置之后,还是通过实现服务、减少函数 proxy、申请 registry 和 reloadInvoker 指向服务端 ip 等操作,重写了客户端实例 userProvider 的对应函数,这时再通过调用 GetUser 函数,能够间接通过 invoker,调用到曾经开启的服务端,实现 rpc 过程。
上面会从 server 端和 client 端两个角度,具体解说服务启动、registry 注册和调用过程。
5. 自定义配置文件(非环境变量)办法
1)服务端自定义配置文件
- var providerConfigStr =
xxxxx
// 配置文件内容,能够参考 log 和 client。在这里你能够定义配置文件的获取形式,比方配置核心,本地文件读取。
log 地址 :https://github.com/dubbogo/dubbo-samples/blob/master/golang/helloworld/dubbo/go-client/profiles/release/log.yml
client 地址 :https://github.com/dubbogo/dubbo-samples/blob/master/golang/helloworld/dubbo/go-client/profiles/release/client.yml
- 在
config.Load()
之前设置配置,例如:
func main() {hessian.RegisterPOJO(&User{})
providerConfig := config.ProviderConfig{}
yaml.Unmarshal([]byte(providerConfigStr), &providerConfig)
config.SetProviderConfig(providerConfig)
defaultServerConfig := dubbo.GetDefaultServerConfig()
dubbo.SetServerConfig(defaultServerConfig)
logger.SetLoggerLevel("warn") // info,warn
config.Load()
select {}}
2)客户端自定义配置文件
- var consumerConfigStr =
xxxxx
// 配置文件内容,能够参考 log 和 clien。在这里你能够定义配置文件的获取形式,比方配置核心,本地文件读取。 - 在
config.Load()
之前设置配置,例如:
func main() {p := config.ConsumerConfig{}
yaml.Unmarshal([]byte(consumerConfigStr), &p)
config.SetConsumerConfig(p)
defaultClientConfig := dubbo.GetDefaultClientConfig()
dubbo.SetClientConf(defaultClientConfig)
logger.SetLoggerLevel("warn") // info,warn
config.Load()
user := &User{}
err := userProvider.GetUser(context.TODO(), []interface{}{"A001"}, user)
if err != nil {log.Print(err)
return
}
log.Print(user)
}
Server 端
服务裸露过程波及到屡次原始 rpcService 的封装、裸露,网上其余文章的图感觉太过抽象,在此,简要地绘制了一个用户定义服务的数据流图:
1. 加载配置
1)框架初始化
在加载配置之前,框架提供了很多已定义好的协定、工厂等组件,都会在对应模块 init 函数内注册到 extension 模块上,以供接下来配置文件中进行选用。
其中重要的有:
- 默认函数代理工厂 :common/proxy/proxy_factory/default.go
func init() {extension.SetProxyFactory("default", NewDefaultProxyFactory)
}
它的作用是将原始 rpc-service 进行封装,造成 proxy_invoker,更易于实现近程 call 调用,详情可见其 invoke 函数。
- 注册核心注册协定 :
registry/protocol/protocol.go
func init() {extension.SetProtocol("registry", GetProtocol)
}
它负责将 invoker 裸露给对应注册核心,比方 zk 注册核心。
- zookeeper 注册协定 :registry/zookeeper/zookeeper.go
func init() {extension.SetRegistry("zookeeper", newZkRegistry)
}
它合并了 base_resiger,负责在服务裸露过程中,将服务注册在 zookeeper 注册器上,从而为调用者提供调用办法。
- dubbo 传输协定 :protocol/dubbo/dubbo.go
func init() {extension.SetProtocol(DUBBO, GetProtocol)
}
它负责监听对应端口,将具体的服务裸露,并启动对应的事件 handler,将近程调用的 event 事件传递到 invoker 外部,调用本地 invoker 并取得执行后果返回。
- filter 包装调用链协定 :protocol/protocolwrapper/protocol_filter_wrapper.go
func init() {extension.SetProtocol(FILTER, GetProtocol)
}
它负责在服务裸露过程中,将代理 invoker 打包,通过配置好的 filter 造成调用链,并交付给 dubbo 协定进行裸露。
上述提前注册好的框架已实现的组件,在整个服务裸露调用链中都会用到,会依据配置取其所需。
2)配置文件
服务端须要的重要配置有三个字段:services、protocols、registries。
profiles/dev/server.yml:
registries :
"demoZk":
protocol: "zookeeper"
timeout : "3s"
address: "127.0.0.1:2181"
services:
"UserProvider":
# 能够指定多个 registry,应用逗号隔开; 不指定默认向所有注册核心注册
registry: "demoZk"
protocol : "dubbo"
# 相当于 dubbo.xml 中的 interface
interface : "com.ikurento.user.UserProvider"
loadbalance: "random"
warmup: "100"
cluster: "failover"
methods:
- name: "GetUser"
retries: 1
loadbalance: "random"
protocols:
"dubbo":
name: "dubbo"
port: 20000
其中 service 指定了要裸露的 rpc-service 名(”UserProvider)、裸露的协定名(”dubbo”)、注册的协定名 (“demoZk”)、裸露的服务所处的 interface、负载平衡策略、集群失败策略及调用的办法等等。
其中,两头服务的协定名须要和 registries 下的 mapkey 对应,裸露的协定名须要和 protocols 下的 mapkey 对应。
能够看到上述例子中,应用了 dubbo 作为裸露协定,应用了 zookeeper 作为两头注册协定,并且给定了端口。如果 zk 须要设置用户名和明码,也能够在配置中写好。
3)配置文件的读入和查看
config/config_loader.go:: Load()
在上述 example 的 main 函数中,有 config.Load() 函数的间接调用,该函数执行细节如下:
// Load Dubbo Init
func Load() {
// init router
initRouter()
// init the global event dispatcher
extension.SetAndInitGlobalDispatcher(GetBaseConfig().EventDispatcherType)
// start the metadata report if config set
if err := startMetadataReport(GetApplicationConfig().MetadataType, GetBaseConfig().MetadataReportConfig); err != nil {logger.Errorf("Provider starts metadata report error, and the error is {%#v}", err)
return
}
// reference config
loadConsumerConfig()
// service config
loadProviderConfig()
// init the shutdown callback
GracefulShutdownInit()}
在本文中,咱们重点关怀 loadConsumerConfig() 和 loadProviderConfig() 两个函数。
对于 provider 端,能够看到 loadProviderConfig() 函数代码如下:
前半部分是配置的读入和查看,进入 for 循环后,是单个 service 的裸露起始点。
后面提到,在配置文件中曾经写好了要裸露的 service 的种种信息,比方服务名、interface 名、method 名等等。在图中 for 循环内,会将所有 service 的服务顺次实现。
for 循环的第一行,依据 key 调用 GetProviderService 函数,拿到注册的 rpcService 实例,这里对应上述提到的 init 函数中,用户手动注册的本人实现的 rpc-service 实例:
这个对象也就成为了 for 循环中的 rpcService 变量,将这个对象注册通过 Implement 函数写到 sys(ServiceConfig 类型)上,设置好 sys 的 key 和协定组,最终调用了 sys 的 Export 办法。
此处对应流程图的局部:
至此,框架配置构造体曾经拿到了所有 service 无关的配置,以及用户定义好的 rpc-service 实例,它触发了 Export 办法,旨在将本人的实例裸露进来。这是 Export 调用链的起始点。
2. 原始 service 封装入 proxy_invoker
config/service_config.go :: Export()
接下来进入 ServiceConfig.Export() 函数.
这个函数进行了一些细碎的操作,比方为不同的协定调配随机端口,如果指定了多个核心注册协定,则会将服务通过多个核心注册协定的 registryProtocol 裸露进来,咱们只关怀对于一个注册协定是如何操作的。还有一些操作比方生成调用 url 和注册 url,用于为裸露做筹备。
1)首先通过配置生成对应 registryUrl 和 serviceUrl
registryUrl 是用来向核心注册组件发动注册申请的,对于 zookeeper 的话,会传入其 ip 和端口号,以及附加的用户名明码等信息。
这个 regUrl 目前只存有注册(zk)相干信息,后续会补写入 ServiceIvk,即服务调用相干信息,外面蕴含了办法名,参数等 …
2)对于一个注册协定,将传入的 rpc-service 实例注册在 common.ServiceMap
这个 Register 函数将服务实例注册了两次,一次是以 Interface 为 key 写入接口服务组内,一次是以 interface 和 proto 为 key 写入特定的一个惟一的服务。
后续会从 common.Map 外面取出来这个实例。
3)获取默认代理工厂,将实例封装入代理 invoker
// 拿到一个 proxyInvoker,这个 invoker 的 url 是传入的 regUrl,这个中央将下面注册的 service 实例封装成了 invoker
// 这个 GetProxyFactory 返回的默认是 common/proxy/proxy_factory/default.go
// 这个默认工厂调用 GetInvoker 取得默认的 proxyInvoker,保留了以后注册 url
invoker := extension.GetProxyFactory(providerConfig.ProxyFactory).GetInvoker(*regUrl)
// 裸露进去 生成 exporter, 开启 tcp 监听
// 这里就该跳到 registry/protocol/protocol.go registryProtocol 调用的 Export,将以后 proxyInvoker 导出
exporter = c.cacheProtocol.Export(invoker)
这一步的 GetProxyFactory(“default”) 办法获取默认代理工厂,通过传入上述结构的 regUrl,将 url 封装入代理 invoker。
能够进入 common/proxy/proxy_factory/default.go::ProxyInvoker.Invoke() 函数里,看到对于 common.Map 取用为 svc 的局部,以及对于 svc 对应 Method 的理论调用 Call 的函数如下:
到这里,下面 GetInvoker(*regUrl) 返回的 invoker 即为 proxy_invoker,它封装好了用户定义的 rpc_service,并将具体的调用逻辑封装入了 Invoke 函数内。
为什么应用 Proxy_invoker 来调用?
通过这个 proxy_invoke 调用用户的性能函数,调用形式将更加抽象化,能够在代码中看到,通过 ins 和 outs 来定义入参和出参,将整个调用逻辑抽象化为 invocation 构造体,而将具体的函数名的抉择、参数向下传递和 reflect 反射过程封装在 invoke 函数内,这样的设计更有利于之后近程调用。集体认为这是 dubbo Invoke 调用链的设计思维。
至此,实现了图中对应的局部:
3. registry 协定在 zkRegistry 上裸露下面的 proxy_invoker
下面,咱们执行到了 exporter = c.cacheProtocol.Export(invoker)。
这里的 cacheProtocol 为一层缓存设计,对应到原始的 demo 上,这里是默认实现好的 registryProtocol。
registry/protocol/protocol.go:: Export()
这个函数内结构了多个 EventListener,十分有 java 的设计感。
咱们只关怀服务裸露的过程,先疏忽这些监听器。
1)获取注册 url 和服务 url
2)获取注册核心实例 zkRegistry
一层缓存操作,如果 cache 没有须要从 common 外面从新拿 zkRegistry。
3)zkRegistry 调用 Registry 办法,在 zookeeper 上注册 dubboPath
上述拿到了具体的 zkRegistry 实例,该实例的定义在:registry/zookeeper/registry.go。
该构造体组合了 registry.BaseRegistry 构造,base 构造定义了注册器根底的性能函数,比方 Registry、Subscribe 等,但在这些默认定义的函数外部,还是会调用 facade 层(zkRegistry 层)的具体实现函数,这一设计模型能在保障已有性能函数不须要反复定义的同时,引入外层函数的实现,相似于构造体继承却又复用了代码。这一设计模式值得学习。
咱们查看上述 registry/protocol/protocol.go:: Export() 函数,间接调用了:
// 1. 通过 zk 注册器,调用 Register() 函数,将已有 @root@rawurl 注册到 zk 上
err := reg.Register(*registeredProviderUrl)
将已有 RegistryUrl 注册到了 zkRegistry 上。
这一步调用了 baseRegistry 的 Register 函数,进而调用 zkRegister 的 DoRegister 函数,进而调用:
在这个函数里,将对应 root 发明一个新的节点。
并且写入具体 node 信息,node 为 url 通过 encode 的后果, 蕴含了服务端的调用形式。
这部分的代码较为简单,具体能够看 baseRegistry 的 processURL() 函数:http://t.tb.cn/6Xje4bijnsIDNaSmyPc4Ot。
至此,将服务端调用 url 注册到了 zookeeper 上,而客户端如果想获取到这个 url,只须要传入特定的 dubboPath,向 zk 申请即可。目前 client 是能够获取到拜访形式了,但服务端的特定服务还没有启动,还没有开启特定协定端口的监听,这也是 registry/protocol/protocol.go:: Export() 函数接下来要做的事件。
4)proxy_invoker 封装入 wrapped_invoker,失去 filter 调用链
// invoker 封装入 warppedInvoker
wrappedInvoker := newWrappedInvoker(invoker, providerUrl)
// 通过为 invoker 减少 filter 调用链,再应用 dubbo 协定 Export,开启 service 并且返回了 Exporter。// export_1
cachedExporter = extension.GetProtocol(protocolwrapper.FILTER).Export(wrappedInvoker)
新建一个 WrappedInvoker,用于之后链式调用。
拿到提前实现并注册好的 ProtocolFilterWrapper,调用 Export 办法,进一步裸露。
protocol/protocolwrapped/protocol_filter_wrapper.go:Export()
protocol/protocolwrapped/protocol_filter_wrapper.go:buildInvokerChain
可见,依据配置的内容,通过链式调用的结构,将 proxy_invoker 层层包裹在调用链的最底部,最终返回一个调用链 invoker。
对应图中局部:
至此,咱们曾经拿到 filter 调用链,期待将这个 chain 裸露到特定端口,用于相应申请事件。
5)通过 dubbo 协定裸露 wrapped_invoker
protocol/protocolwrapped/protocol_filter_wrapper.go:Export()
// 通过 dubbo 协定 Export dubbo_protocol 调用的 export_2
return pfw.protocol.Export(invoker)
回到上述 Export 函数的最初一行,调用了 dubboProtocol 的 Export 办法,将上述 chain 真正裸露。
该 Export 办法的具体实现在:protocol/dubbo/dubbo_protocol.go: Export()。
这一函数做了两个事件:结构触发器、启动服务。
- 将传入的 Invoker 调用 chain 进一步封装,封装成一个 exporter,再将这个 export 放入 map 保留。 留神!这里把 exporter 放入了 SetExporterMap 中,在上面服务启动的时候,会以注册事件监听器的模式将这个 exporter 取出!
- 调用 dubboProtocol 的 openServer 办法,开启一个针对特定端口的监听。
如上图所示,一个 Session 被传入,开启对应端口的事件监听。
至此结构出了 exporter,实现图中局部:
4. 注册触发动作
上述只是启动了服务,但还没有看到触发事件的细节,点进下面的 s.newSession 能够看到,dubbo 协定为一个 getty 的 session 默认应用了如下配置:
其中很重要的一个配置是 EventListener,传入的是 dubboServer 的默认 rpcHandler。
protocol/dubbo/listener.go:OnMessage()
rpcHandler 有一个实现好的 OnMessage 函数,依据 getty 的 API,当 client 调用该端口时,会触发 OnMessage。
// OnMessage notified when RPC server session got any message in connection
func (h *RpcServerHandler) OnMessage(session getty.Session, pkg interface{}) {
这一函数实现了在 getty session 接管到 rpc 调用后的一系列解决:
- 传入包的解析
- 依据申请包结构申请 url
- 拿到对应申请 key,找到要被调用的 exporter
- 拿到对应的 Invoker
- 结构 invocation
- 调用
- 返回
整个被调过程零打碎敲。实现了从 getty.Session 的调用事件,到通过层层封装的 invoker 的调用。
至此,一次 rpc 调用得以正确返回。
小结
- 对于 Invoker 的层层封装
能把一次调用形象成一次 invoke;能把一个协定形象成针对 invoke 的封装;能把针对一次 invoke 所做出的特定扭转封装到 invoke 函数外部,能够升高模块之间的耦合性。层层封装逻辑更加清晰。
- 对于 URL 的形象
对于 dubbo 的统一化申请对象 URL 的极度形象是之前没有见过的 … 集体认为这样封装能保障申请参数列表的简化和统一。但在开发的过程中,滥用极度形象的接口可能造成 … debug 的艰难?以及不晓得哪些字段是以后曾经封装好的,哪些字段是无用的。
- 对于协定的了解
之前了解的协定还是太过具体化了,而对于 dubbo-go 对于 dubboProtocol 的协定,我认为是基于 getty 的进一步封装,它定义了客户端和服务端,对于 getty 的 session 应该有哪些特定的操作,从而保障主和谐被调的协定一致性,而这种保障也是一种协定的体现,是由 dubbo 协定来标准的。
如果你有任何疑难,欢送钉钉扫码退出交换群:钉钉群号 23331795!
作者简介
李志信 (GitHubID LaurenceLiZhixin),中山大学软件工程业余在校学生,善于应用 Java/Go 语言,专一于云原生和微服务等技术方向。
“阿里巴巴云原生关注微服务、Serverless、容器、Service Mesh 等技术畛域、聚焦云原生风行技术趋势、云原生大规模的落地实际,做最懂云原生开发者的公众号。”