在阅读了etcd server的启动流程后,我们对很多关键性函数的入口都有了初步印象。
那么,接下来我们一起看看对键值对的修改,在etcd server内部是怎么流转的。
用etcdctl
这个指令,我们可以快速地用命令etcdctl put key value
发送PUT键值对的请求。
但etcdctl
对请求做了封装,我们要了解原始的HTTP请求格式,才能方便地阅读相关代码。相关的途径有很多,比如抓包、读源码等,这里为了可阅读性,我给出一个curl
请求。
curl -L http://localhost:2379/v3/kv/put -X POST -d '{"key":"mykey","value":"myvalue"}'
主要关注如下三点:
POST
/v3/kv/put
{"key":"mykey","value":"myvalue"}
这个请求是v3版本的,而v2版本的差异比较大,暂不细谈。
为了更好地介绍下面的内容,我先介绍mux下的2个概念。
pattern
指的是一种URL的匹配模式,最常见的如全量匹配、前缀匹配、正则匹配。当一个请求进来时,它会有自己的一个URL,去匹配mux
中预先定义的多个pattern
,找到一个最合适的。这是一种URL路由规则的实现。pattern
后,就会执行它预定义的handler
,也就是一个处理函数,返回结果。所以, pattern
负责匹配,而handler
负责执行。在不同语境下,它们的专业术语有所差异,大家自行对应即可。
我们要找HTTP1.X的路由匹配逻辑,就回到了上一节最后看到的代码中:
// 创建路由匹配规则
httpmux := sctx.createMux(gwmux, handler)
// 新建http server对象
srvhttp := &http.Server{
Handler: createAccessController(sctx.lg, s, httpmux),
ErrorLog: logger, // do not log user error
}
// 这个cumx.HTTP1是检查协议是否满足HTTP1
httpl := m.Match(cmux.HTTP1())
// 运行server
go func() { errHandler(srvhttp.Serve(httpl)) }()
本函数不长,但很容易让读源码的同学陷入误区,我们一起来看看。这块代码主要分为三段:
func (sctx *serveCtx) createMux(gwmux *gw.ServeMux, handler http.Handler) *http.ServeMux {
httpmux := http.NewServeMux()
// 1.注册handler
for path, h := range sctx.userHandlers {
httpmux.Handle(path, h)
}
// 2.注册grpcGateway mux中的handler到/v3/路径下
if gwmux != nil {
httpmux.Handle(
"/v3/",
wsproxy.WebsocketProxy(
gwmux,
wsproxy.WithRequestMutator(
// Default to the POST method for streams
func(_ *http.Request, outgoing *http.Request) *http.Request {
outgoing.Method = "POST"
return outgoing
},
),
wsproxy.WithMaxRespBodyBufferSize(0x7fffffff),
),
)
}
// 3.注册根路径下的handler
if handler != nil {
httpmux.Handle("/", handler)
}
return httpmux
}
第一点,可以通过简单的代码阅读,看到是对pprof
和debug
这些通用功能的URL功能注册,也是一些用户自定义的handler
注册,这就很好地对应到sctx.userHandlers
这个变量的命名了。
第三点很快就能被排除,它注册的是对根路径下的handler。我们阅读代码,找到handler最原始的生成处,就能看到它是对version、metrcis这类handler的注册。
所以,我们的重点就放在了gwmux
这个对象上。阅读它的创建过程,就得跳转到上层函数。
在函数中,我们可以看到它注册了一个类型为registerHandlerFunc
的handlers列表,包括如下内容:
handlers := []registerHandlerFunc{
etcdservergw.RegisterKVHandler, // KV键值对的处理
etcdservergw.RegisterWatchHandler, // Watch监听
etcdservergw.RegisterLeaseHandler, // Lease租约
etcdservergw.RegisterClusterHandler, // 集群
etcdservergw.RegisterMaintenanceHandler, // 维护相关
etcdservergw.RegisterAuthHandler, // 认证
v3lockgw.RegisterLockHandler, // 锁
v3electiongw.RegisterElectionHandler, // 选举
}
for _, h := range handlers {
if err := h(ctx, gwmux, conn); err != nil {
return nil, err
}
}
我们聚焦到PUT请求的处理,它自然走的是etcdservergw.RegisterKVHandler
这个入口。
本函数位于etcd/etcdserver/etcdserverpb/gw/rpc.pb.gw.go
。它其实是用protobuf自动生成的,其中用到了grpc-gateway
这个关键性技术,它的作用是将HTTP1的请求转换成gRPC,实现一个server可以同时支持HTTP1与gRPC,并且只写一份gRPC处理的代码即可。
有兴趣地可以去看看 https://github.com/grpc-ecosystem/grpc-gateway 项目。 大致调用链路为: HTTP1 -> gRPC -> 自己实现的handler
该函数是由proto文件生成的,这里我忽略了关于context的处理,提取关键性的内容:
mux.Handle("POST", pattern_KV_Put_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
// 反序列化请求和序列化响应
inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
// 执行PUT请求
resp, md, err := request_KV_Put_0(rctx, inboundMarshaler, client, req, pathParams)
// 返回结果
forward_KV_Put_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
})
序列化与反序列化存在多种选择,我们暂不深入,先来看看处理这部分的工作:
首先是如何匹配请求,也就是http://localhost:2379/v3/kv/put
,对应如下:
var pattern_KV_Put_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v3", "kv", "put"}, ""))
而最核心的处理,也就是解析PUT请求的函数request_KV_Put_0
与返回处理结果的函数forward_KV_Put_0
,我们放到下一讲再来看。
今天我们看了PUT
请求在etcd server中通过mux
的匹配逻辑,思路参考下图。
在阅读代码期间,我们接触到了grpc-gateway这个技术方案,有兴趣的朋友可以参考我的另一篇文章。
Github: https://github.com/Junedayday/code_reading Blog: http://junes.tech/ Bilibili: https://space.bilibili.com/293775192