背景
因为公司外部所有服务都是跑在阿里云 k8s 上的,而后 dubbo 提供者默认向注册核心上报的 IP 都是 Pod IP
,这意味着在 k8s 集群外的网络环境是调用不了 dubbo 服务的,如果本地开发须要拜访 k8s 内的 dubbo 提供者服务的话,须要手动把服务裸露到外网,咱们的做法是针对每一个提供者服务裸露一个SLB IP+ 自定义端口
,并且通过 dubbo 提供的DUBBO_IP_TO_REGISTRY
和DUBBO_PORT_TO_REGISTRY
环境变量来把对应的 SLB IP+ 自定义端口
注册到注册中心里,这样就实现了本地网络和 k8s dubbo 服务的买通,然而这种形式治理起来十分麻烦,每个服务都得自定义一个端口,而且每个服务之间端口还不能抵触,当服务多起来之后十分难以治理。
于是我就在想能不能像 nginx ingress
一样实现一个 七层代理 + 虚构域名
来复用一个端口,通过指标 dubbo 提供者的 application.name
来做对应的转发,这样的话所有的服务只须要注册同一个 SLB IP+ 端口
就能够了,大大的晋升便利性,一方调研之后发现可行就开撸了!
我的项目已开源:https://github.com/monkeyWie/dubbo-ingress-controller
技术预研
思路
- 首先 dubbo RPC 调用默认是走的
dubbo 协定
,所以我须要先去看看协定里有没有能够利用做转发的报文信息,就是寻找相似于 HTTP 协定里的 Host 申请头,如果有的话就能够依据此信息做反向代理
和虚构域名
的转发,在此基础之上实现一个相似nginx
的dubbo 网关
。 - 第二步就是要实现
dubbo ingress controller
,通过 k8s ingress 的 watcher 机制动静的更新dubbo 网关
的虚构域名转发配置,而后所有的提供者服务都由此服务同一转发,并且上报到注册核心的地址也对立为此服务的地址。
架构图
dubbo 协定
先上一个官网的协定图:
能够看到 dubbo 协定的 header 是固定的 16 个字节
,外面并没有相似于 HTTP Header 的可扩大字段,也没有携带指标提供者的application.name
字段,于是我向官网提了个 issue,官网的回答是通过消费者 自定义 Filter
来将指标提供者的 application.name
放到 attachments
里,这里不得不吐槽下 dubbo 协定,扩大字段居然是放在 body
里,如果要实现转发须要把申请报文全副解析完能力拿到想要报文,不过问题不大,因为次要是做给开发环境用的,这一步勉强能够实现。
k8s ingress
k8s ingress 是为 HTTP 而生的,然而外面的字段够用了,来看一段 ingress 配置:
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: user-rpc-dubbo
annotations:
kubernetes.io/ingress.class: "dubbo"
spec:
rules:
- host: user-rpc
http:
paths:
- backend:
serviceName: user-rpc
servicePort: 20880
path: /
配置和 http 一样通过 host
来做转发规定,然而 host
配置的是指标提供者的 application.name
,后端服务是指标提供者对应的service
,这里有一个比拟非凡的是应用了一个kubernetes.io/ingress.class
注解,这个注解能够指定此 ingress
对哪个 ingress controller
失效,前面咱们的 dubbo ingress controller
就只会解析注解值为 dubbo
的 ingress 配置。
开发
后面的技术预研一切顺利,接着就进入开发阶段了。
消费者自定义 Filter
后面有提到如果申请里要携带指标提供者的 application.name
,须要消费者 自定义 Filter
,代码如下:
@Activate(group = CONSUMER)
public class AddTargetFilter implements Filter {
@Override
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {String targetApplication = StringUtils.isBlank(invoker.getUrl().getRemoteApplication()) ?
invoker.getUrl().getGroup() : invoker.getUrl().getRemoteApplication();
// 指标提供者的 application.name 放入 attachment
invocation.setAttachment("target-application", targetApplication);
return invoker.invoke(invocation);
}
}
这里又要吐槽一下,dubbo 消费者首次拜访时会发动一个获取 metadata 的申请,这个申请通过 invoker.getUrl().getRemoteApplication()
是拿不到值的,通过 invoker.getUrl().getGroup()
能力拿到。
dubbo 网关
这里就要开发一个相似 nginx
的dubbo 网关
,并实现七层代理和虚构域名转发,编程语言间接抉择了 go,首先 go 做网络开发心智累赘低,另外有个 dubbo-go 我的项目,能够间接利用外面的解码器,而后 go 有原生的 k8s sdk 反对,几乎完满!
思路就是开启一个 TCP Server
,而后解析 dubbo 申请的报文,把attachment
里的 target-application
属性拿到,再反向代理到真正的 dubbo 提供者服务上,外围代码如下:
routingTable := map[string]string{
"user-rpc": "user-rpc:20880",
"pay-rpc": "pay-rpc:20880",
}
listener, err := net.Listen("tcp", ":20880")
if err != nil {return err}
for {clientConn, err := listener.Accept()
if err != nil {logger.Errorf("accept error:%v", err)
continue
}
go func() {defer clientConn.Close()
var proxyConn net.Conn
defer func() {
if proxyConn != nil {proxyConn.Close()
}
}()
scanner := bufio.NewScanner(clientConn)
scanner.Split(split)
// 解析申请报文,拿到一个残缺的申请
for scanner.Scan() {data := scanner.Bytes()
// 通过 dubbo-go 提供的库把[]byte 反序列化成 dubbo 申请构造体
buf := bytes.NewBuffer(data)
pkg := impl.NewDubboPackage(buf)
pkg.Unmarshal()
body := pkg.Body.(map[string]interface{})
attachments := body["attachments"].(map[string]interface{})
// 从 attachments 里拿到指标提供者的 application.name
target := attachments["target-application"].(string)
if proxyConn == nil {
// 反向代理到真正的后端服务上
host := routingTable[target]
proxyConn, _ = net.Dial("tcp", host)
go func() {
// 原始转发
io.Copy(clientConn, proxyConn)
}()}
// 把原始报文写到真正后端服务上,而后走原始转发即可
proxyConn.Write(data)
}
}()}
func split(data []byte, atEOF bool) (advance int, token []byte, err error) {if atEOF && len(data) == 0 {return 0, nil, nil}
buf := bytes.NewBuffer(data)
pkg := impl.NewDubboPackage(buf)
err = pkg.ReadHeader()
if err != nil {if errors.Is(err, hessian.ErrHeaderNotEnough) || errors.Is(err, hessian.ErrBodyNotEnough) {return 0, nil, nil}
return 0, nil, err
}
if !pkg.IsRequest() {return 0, nil, errors.New("not request")
}
requestLen := impl.HEADER_LENGTH + pkg.Header.BodyLen
if len(data) < requestLen {return 0, nil, nil}
return requestLen, data[0:requestLen], nil
}
dubbo ingress controller 实现
后面曾经实现了一个 dubbo 网关
,然而外面的虚构域名转发配置(routingTable
) 还是写死在代码里的,当初要做的就是当检测到 k8s ingress
有更新时,动静的更新这个配置就能够了。
首先先简略的阐明下 ingress controller
的原理,拿咱们罕用的 nginx ingress controller
为例,它也是一样通过监听 k8s ingress
资源变动,而后动静的生成 nginx.conf
文件,当发现配置产生了扭转时,触发 nginx -s reload
从新加载配置文件。
外面用到的核心技术就是 informers,利用它来监听 k8s 资源
的变动,示例代码:
// 在集群内获取 k8s 拜访配置
cfg, err := rest.InClusterConfig()
if err != nil {logger.Fatal(err)
}
// 创立 k8s sdk client 实例
client, err := kubernetes.NewForConfig(cfg)
if err != nil {logger.Fatal(err)
}
// 创立 Informer 工厂
factory := informers.NewSharedInformerFactory(client, time.Minute)
handler := cache.ResourceEventHandlerFuncs{AddFunc: func(obj interface{}) {// 新增事件},
UpdateFunc: func(oldObj, newObj interface{}) {// 更新事件},
DeleteFunc: func(obj interface{}) {// 删除事件},
}
// 监听 ingress 变动
informer := factory.Extensions().V1beta1().Ingresses().Informer()
informer.AddEventHandler(handler)
informer.Run(ctx.Done())
通过实现下面的三个事件来动静的更新转发配置,每个事件都会携带对应的 Ingress
对象信息过去,而后进行对应的解决即可:
ingress, ok := obj.(*v1beta12.Ingress)
if ok {
// 通过注解过滤出 dubbo ingress
ingressClass := ingress.Annotations["kubernetes.io/ingress.class"]
if ingressClass == "dubbo" && len(ingress.Spec.Rules) > 0 {rule := ingress.Spec.Rules[0]
if len(rule.HTTP.Paths) > 0 {backend := rule.HTTP.Paths[0].Backend
host := rule.Host
service := fmt.Sprintf("%s:%d", backend.ServiceName+"."+ingress.Namespace, backend.ServicePort.IntVal)
// 获取到 ingress 配置中 host 对应的 service,告诉给 dubbo 网关进行更新
notify(host,service)
}
}
}
docker 镜像提供
k8s 之上所有的服务都须要跑在容器里的,这里也不例外,须要把 dubbo ingress controller
构建成 docker 镜像,这里通过两阶段构建优化,来减小镜像体积:
FROM golang:1.17.3 AS builder
WORKDIR /src
COPY . .
ENV GOPROXY https://goproxy.cn
ENV CGO_ENABLED=0
RUN go build -ldflags "-w -s" -o main cmd/main.go
FROM debian AS runner
ENV TZ=Asia/shanghai
WORKDIR /app
COPY --from=builder /src/main .
RUN chmod +x ./main
ENTRYPOINT ["./main"]
yaml 模板提供
因为要在集群内拜访 k8s API,须要给 Pod 进行受权,通过 K8S rbac
进行受权,并以 Deployment
类型服务进行部署,最终模板如下:
apiVersion: v1
kind: ServiceAccount
metadata:
name: dubbo-ingress-controller
namespace: default
---
kind: ClusterRole
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: dubbo-ingress-controller
rules:
- apiGroups:
- extensions
resources:
- ingresses
verbs:
- get
- list
- watch
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: dubbo-ingress-controller
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: dubbo-ingress-controller
subjects:
- kind: ServiceAccount
name: dubbo-ingress-controller
namespace: default
---
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: default
name: dubbo-ingress-controller
labels:
app: dubbo-ingress-controller
spec:
selector:
matchLabels:
app: dubbo-ingress-controller
template:
metadata:
labels:
app: dubbo-ingress-controller
spec:
serviceAccountName: dubbo-ingress-controller
containers:
- name: dubbo-ingress-controller
image: liwei2633/dubbo-ingress-controller:0.0.1
ports:
- containerPort: 20880
前期需要的话能够做成 Helm
进行治理。
后记
至此 dubbo ingress controller
实现实现,能够说麻雀虽小然而五脏俱全,外面波及到了 dubbo 协定
、TCP 协定
、 七层代理
、k8s ingress
、docker
等等很多内容,这些很多常识都是在 云原生
越来越风行的时代须要把握的,开发完之后感觉受益匪浅。
对于残缺的应用教程能够通过 github 查看。
参考链接:
- dubbo 协定
- dubbo-go
- 应用多个 -ingress- 控制器
- 应用 Golang 自定义 Kubernetes Ingress Controller
我是 MonkeyWie,欢送扫码👇👇关注!不定期在公众号中分享
JAVA
、Golang
、前端
、docker
、k8s
等干货常识。