今天来撸一个简单的ingress-controller, 用于了解ingress-controller的实现机制。
完整的代码在 simple-ingress-controller
基本逻辑
ingress说的简单点就是url 和 service的对应关系,相当于nginx的upstream,而ingress-controller就是用来管理这些“upstream”的,可以理解为是个动态的反向代理。
基于“动态反向代理”这个概念,实现一个简单的ingress-controller需要2部分,即 动态 + 反向代理,所以可以分开进行实现。
反向代理
同样使用go来写,简单的反向代理golang 有现成的方法,可以参照我之前写的golang的一个简单的webproxy的代码: 简单的webproxy
这里贴下主要代码
// 实际的代理服务功能
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// 获取后端的真实服务地址
backendURL, err := s.routingTables.GetBackend(r.Host, r.URL.Path)
if err != nil {
http.Error(w, "upstream server not found", http.StatusNotFound)
return
}
klog.Infof("[webproxy] get proxy request from: %s%s to: %v", r.Host, r.URL.Path, backendURL)
// 使用 NewSingleHostReverseProxy 进行代理请求
p := httputil.NewSingleHostReverseProxy(backendURL)
p.ServeHTTP(w, r)
}
主要使用的就是httputil.NewSingleHostReverseProxy 这个方法,进行反向代理的转发,将用户请求的url转发到真实url 上。
由于只是个简单的webproxy,所以对应的url 和转发策略都是静态的
// 初始化一个新的路由表
func NewRoutingTable() *RoutingTable {
rt := &RoutingTable{
//certificatesByHost: make(map[string]map[string]*tls.Certificate),
Backends: make(map[string][]routingTableBackend),
}
// 真实的服务器host + 端口
rtb, _ := newroutingTableBackend("hello", "127.0.0.1", 12345)
rt.Backends["www.zyx.com"] = append(rt.Backends["www.zyx.com"], rtb)
return rt
}
动态
有了一个简单的proxy,接下来就要考虑如何动态的生成 upstream,回忆之前有提到k8s的informer的概念,如何动态感知资源的变化*,而动态的upstream不就是通过监听资源变化动态加载到代理的backend里么,所以ingress-controller的动态操作可以使用informer进行实现。
// 每10分钟去list一下
factory := informers.NewSharedInformerFactory(w.client, 10 * time.Minute)
secretLister := factory.Core().V1().Secrets().Lister()
serviceLister := factory.Core().V1().Services().Lister()
ingressLister := factory.Extensions().V1beta1().Ingresses().Lister()
定义好informer factory, 然后根据需要的资源生产监听器,然后设置事件。
onChange := func() {
payload := &Payload{
TLSCertificates: make(map[string]*tls.Certificate),
}
// 获得所有的 Ingress
ingresses, err := ingressLister.List(labels.Everything())
if err != nil {
klog.Errorf("[ingress] failed to list ingresses")
return
}
for _, ingress := range ingresses {
ingressPayload := IngressPayload{
Ingress: ingress,
}
payload.Ingresses = append(payload.Ingresses, ingressPayload)
for _, rule := range ingress.Spec.Rules {
if rule.HTTP == nil {
klog.Errorf("[ingress] http rule is nil, do not make the payload")
continue
}
for _, path := range rule.HTTP.Paths {
// 给 ingressPayload 组装数据
addBackend(&ingressPayload, rule.Host, path.Path, path.Backend)
}
}
// 证书处理
for _, rec := range ingress.Spec.TLS {
if rec.SecretName != "" {
// 获取证书对应的 secret
secret, err := secretLister.Secrets(ingress.Namespace).Get(rec.SecretName)
if err != nil {
klog.Errorf("[ingress] 获取secret 失败, %v", err)
continue
}
// 加载证书
cert, err := tls.X509KeyPair(secret.Data["tls.crt"], secret.Data["tls.key"])
if err != nil {
klog.Errorf("[ingress] 加载证书失败, %v", err)
continue
}
payload.TLSCertificates[rec.SecretName] = &cert
}
}
}
onChange 这个方法就是 具体的事件处理了,或者说,动态加载ingress 信息到backend 其实就是这个方法做的,考虑到这只是一个最简单的ingress-controller,所以secret、ingress、service 的 Informer都只用这个方法进行处理。
var wg sync.WaitGroup
wg.Add(1)
go func() {
informer := factory.Core().V1().Secrets().Informer()
informer.AddEventHandler(handler)
informer.Run(ctx.Done())
wg.Done()
}()
wg.Add(1)
go func() {
informer := factory.Extensions().V1beta1().Ingresses().Informer()
informer.AddEventHandler(handler)
informer.Run(ctx.Done())
wg.Done()
}()
wg.Add(1)
go func() {
informer := factory.Core().V1().Services().Informer()
informer.AddEventHandler(handler)
informer.Run(ctx.Done())
wg.Done()
}()
wg.Wait()
合并
上面已经有了“反向代理” 和 “动态upstream”的代码了,之后要考虑的就是合并的问题了,将“动态”的结构体指针放在“反向代理”的路由表结构体内就行了,记得加锁,避免线程安全。
watcher的结构体:
// 整体的payload,用于将证书和ingress列表做关联
type Payload struct {
Ingresses []IngressPayload
TLSCertificates map[string]*tls.Certificate
}
// ingress payload, 记录了ingress本体以及他映射的端口
type IngressPayload struct {
Ingress *extensionsv1beta1.Ingress
Host string
Path string
SvcName string
SvcPort int
}
backend路由表的结构体
// 对应ingress rules里的规则,一个host 会匹配多个path
type RoutingTable struct {
CertificatesByHost map[string]map[string]*tls.Certificate
Backends map[string][]routingTableBackend
Lock *sync.RWMutex
}
// 初始化一个新的路由表
func NewRoutingTable(payload *watcher.Payload) *RoutingTable {
rt := &RoutingTable{
CertificatesByHost: make(map[string]map[string]*tls.Certificate),
Backends: make(map[string][]routingTableBackend),
Lock: &sync.RWMutex{},
}
// 第一次加载数据
rt.init(payload)
return rt
}
然后在服务启动的时候,分别启动proxy server 和 watcher 服务,分为2个协程启动
// 初始化 proxy server
s := server.NewServer(port, tlsPort)
// 初始化 watcher 进程
w := watcher.New(client, func(payload *watcher.Payload) {s.Update(payload)})
// 多协程启动
var eg errgroup.Group
eg.Go(func() error {
return s.Run(context.TODO())
})
eg.Go(func() error {
return w.Run(context.TODO())
})
if err := eg.Wait(); err != nil {
klog.Fatalf("[ingress] something is wrong: %v", err.Error())
}
至此,一个简单的ingress-controller就完成了,打包成容器镜像发布到kubernetes内部即可,但需要注意的是,由于该ingress-controller需要watch 和 list的权限,所以在部署的时候需要赋予rbac 的权限。
当然代码只是个demo, 比较粗糙,比如所有informer绑定的都是同一个onchange的方法,比如proxy和watcher 服务是拆成2个协程的,所以加了锁,影响了一点性能等,但拆开编写比较理解,通过这种结构,可以比较容易的编写和理解ingress-controller的概念和原理,实际使用可以添加自己需要的功能,比如tcp/udp的转发,比如支持https的转发,比如混合云场景的使用等。