Fork me on GitHub

2023年11月

学习 Kubernetes 流量管理之 Ingress

学习 Kubernetes 流量管理之 Service 这篇笔记中我们学习了 Kubernetes 是如何使用 Service 进行流量管理的,我们可以通过 NodePortLoadBalancer 这两种类型的 Service 让应用程序暴露到集群外部,不过这两种方式都有各自的问题:

  • NodePort 会在所有节点上暴露端口,外部应用需要知道集群内部节点的 IP 才能访问,一旦集群节点发生变化,外部应用也会受影响,可用性无法保证;而且端口的范围是受限的,默认只能使用 30000 到 32767 之间的端口,外部应用使用起来会感觉怪怪的;另外,每个端口只能对应一个 Service,如果 Service 数量过多,暴露的端口也会过多,不仅安全性难以保障,而且管理起来也会很麻烦;
  • LoadBalancer 依赖于外部负载均衡器作为流量的入口,它在云平台中使用非常广泛,一般使用云供应商提供的 LB 服务,它会有一个自己的 IP 地址来转发所有流量,不过要注意的是,你暴露的每个 Service 都对应一个 LB 服务,而每个 LB 都需要独立付费,如果你暴露的 Service 很多,这将是非常昂贵的。

什么是 Ingress

为了解决上面的问题,Kubernetes 提出了一种新的 API 对象,叫做 Ingress,它通过定义不同的 HTTP 路由规则,将集群内部的 Service 通过 HTTP 的方式暴露到集群外部:

ingress.png

可以将 Ingress 理解为 Service 的网关,它是所有流量的入口,通过 Ingress 我们就能以一个集群外部可访问的 URL 来访问集群内部的 Service,不仅如此,它还具有如下特性:

  • Load Balancing
  • SSL Termination
  • Name-based Virtual Hosting

Ingress 实践

这一节将继续延用之前的 kubernetes-bootcamp 示例,通过 Ingress 将应用程序暴露到集群外部访问。

部署 Ingress Controller

Ingress 本身其实并不具备集群内外通信的能力,它只是一系列的路由转发规则而已,要让这些路由规则生效,必须先部署 Ingress Controller 才行。

由 Kubernetes 支持和维护的 Ingress Controller 有三个:

除此之外,这里 还列出了很多由第三方社区维护的其他 Ingress Controller 可供选择。

下面我们就以 Ingress NGINX Controller 为例,学习如何部署 Ingress Controller。

目前有两个基于 Nginx 实现的 Ingress Controller 比较有名,一个是由 Kubernetes 官方维护的 kubernetes/ingress-nginx,被称为 Ingress NGINX Controller,另一个是由 Nginx 官方维护的 nginxinc/kubernetes-ingress,被称为 NGINX Ingress Controller,两者在技术实现和功能特性上有很多区别,大家在使用时要特别留意。

根据 Ingress NGINX Controller 官方的部署文档,我们大致有两种方式来部署它,第一种是通过 Helm 部署:

# helm upgrade --install ingress-nginx ingress-nginx \
  --repo https://kubernetes.github.io/ingress-nginx \
  --namespace ingress-nginx --create-namespace

第二种是通过 kubectl apply 部署,我比较喜欢这种方式,可以从 YAML 中看到整个部署的细节:

# kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.8.2/deploy/static/provider/cloud/deploy.yaml
namespace/ingress-nginx created
serviceaccount/ingress-nginx created
serviceaccount/ingress-nginx-admission created
role.rbac.authorization.k8s.io/ingress-nginx created
role.rbac.authorization.k8s.io/ingress-nginx-admission created
clusterrole.rbac.authorization.k8s.io/ingress-nginx created
clusterrole.rbac.authorization.k8s.io/ingress-nginx-admission created
rolebinding.rbac.authorization.k8s.io/ingress-nginx created
rolebinding.rbac.authorization.k8s.io/ingress-nginx-admission created
clusterrolebinding.rbac.authorization.k8s.io/ingress-nginx created
clusterrolebinding.rbac.authorization.k8s.io/ingress-nginx-admission created
configmap/ingress-nginx-controller created
service/ingress-nginx-controller created
service/ingress-nginx-controller-admission created
deployment.apps/ingress-nginx-controller created
job.batch/ingress-nginx-admission-create created
job.batch/ingress-nginx-admission-patch created
ingressclass.networking.k8s.io/nginx created
validatingwebhookconfiguration.admissionregistration.k8s.io/ingress-nginx-admission created

从上面的输出可以看到,Ingress NGINX Controller 首先创建一个名为 ingress-nginx 的命名空间,然后在这个命名空间下创建了一堆相关的资源,包括 ServiceAccount、Role、ConfigMap、Deployment、Service、Job 等等,这中间,最重要的是 deployment.apps/ingress-nginx-controllerservice/ingress-nginx-controller 这两项;其实,Ingress Controller 本质上就是一个 Deployment 加上一个 Service,这个 Deployment 通过监听 Ingress 对象的变动来更新路由规则,而用户访问集群的入口仍然是通过 Service 实现的,所以想让用户通过 Ingress 来访问集群,还是得靠 Service 的两种外部通信方式:NodePortLoadBalancer

查看上面这个 YAML,可以发现它使用的就是 LoadBalancer 类型的 Service,一般适用于云环境,如果你没有云环境,官方也提供了几种在物理机环境部署的方式:

其中最简单的方式是使用 NodePort 类型的 Service,直接使用下面这个 YAML 部署即可:

# kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.8.2/deploy/static/provider/baremetal/deploy.yaml

部署完成后,通过下面的命令检查 Ingress NGINX Controller 是否运行成功:

# kubectl get deployment -n ingress-nginx
NAME                       READY   UP-TO-DATE   AVAILABLE   AGE
ingress-nginx-controller   1/1     1            1           29h

通过下面的命令确定 Ingress NGINX Controller 的 NodePort 是多少:

# kubectl get svc -n ingress-nginx
NAME                                 TYPE        CLUSTER-IP    EXTERNAL-IP   PORT(S)                      AGE
ingress-nginx-controller             NodePort    10.96.0.183   <none>        80:26360/TCP,443:23476/TCP   29h
ingress-nginx-controller-admission   ClusterIP   10.96.1.25    <none>        443/TCP                      29h

此时,我们就可以通过 NodePort 来访问集群了,只不过因为我们还没有配置任何路由,所以访问会报 404 Not Found

# curl http://172.31.164.40:26360
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx</center>
</body>
</html>

注意这里实际上暴露了两个 NodePort,一个是 HTTP 端口,另一个是 HTTPS 端口,这个 HTTPS 端口我们也可以访问(-k 表示忽略证书校验):

# curl -k https://172.31.164.40:23476
<html>
<head><title>404 Not Found</title></head>
<body>
<center><h1>404 Not Found</h1></center>
<hr><center>nginx</center>
</body>
</html>

创建 Ingress

接下来,我们创建一个简单的路由规则来验证 Ingress 是否有效:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-ingress
spec:
  ingressClassName: nginx
  rules:
  - http:
      paths:
      - path: /hello
        pathType: Prefix
        backend:
          service:
            name: myapp
            port:
              number: 38080

这个路由规则很容易理解,就是将 /hello 路径映射到后端名为 myapp 的 Service 的 38080 端口。在使用 Ingress 时要注意你的 Kubernetes 版本,不同的 Kubernetes 版本中 Ingress 的 apiVersion 字段略有不同:

Kubernetes 版本Ingress 的 apiVersion
v1.5 - v1.17extensions/v1beta1
v1.8 - v1.18networking.k8s.io/v1beta1
v1.19+networking.k8s.io/v1

另一点值得注意的是 ingressClassName: nginx 这个配置,细心的同学可能已经发现,在上面部署 Ingress NGINX Controller 的时候,默认还创建了一个 IngressClass 资源:

apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
  labels:
    app.kubernetes.io/component: controller
    app.kubernetes.io/instance: ingress-nginx
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
    app.kubernetes.io/version: 1.8.2
  name: nginx
spec:
  controller: k8s.io/ingress-nginx

我们可以将 IngressClass 理解成面向对象中的类这个概念,而 Ingress 则是类的具体示例。在 Ingress NGINX Controller 的启动参数里,我们能看到 --ingress-class=nginx 这样的参数:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: ingress-nginx-controller
  namespace: ingress-nginx
spec:
  minReadySeconds: 0
  revisionHistoryLimit: 10
  template:
    spec:
      containers:
      - args:
        - /nginx-ingress-controller
        - --election-id=ingress-nginx-leader
        - --controller-class=k8s.io/ingress-nginx
        - --ingress-class=nginx

表示它会监听名为 nginxIngressClass,一个集群中可能会部署多个 Ingress Controller,这样就会有多个 IngressClass,所以上面创建 Ingress 时指定 ingressClassName: nginx 表示将这个路由规则应用到刚部署的 Ingress NGINX Controller。

通过 curl 验证 Ingress 是否生效:

# curl http://172.31.164.40:26360/hello
Hello Kubernetes bootcamp! | Running on: myapp-b9744c975-9xm5j | v=1

可以看出,虽然 myapp 这个 Service 类型为 ClusterIP,但是通过 Ingress 我们也可以从集群外部对其进行访问了。

默认 IngressClass

我们可以给某个 IngressClass 加上 ingressclass.kubernetes.io/is-default-class 注解,并将值设置为字符串 "true",表示这是集群中默认的 IngressClass

apiVersion: networking.k8s.io/v1
kind: IngressClass
metadata:
  annotations:
    ingressclass.kubernetes.io/is-default-class: "true"
  labels:
    app.kubernetes.io/component: controller
    app.kubernetes.io/instance: ingress-nginx
    app.kubernetes.io/name: ingress-nginx
    app.kubernetes.io/part-of: ingress-nginx
    app.kubernetes.io/version: 1.8.2
  name: nginx
spec:
  controller: k8s.io/ingress-nginx

当集群中存在默认的 IngressClass 时,创建 Ingress 时就可以不用指定 ingressClassName 参数了:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: my-ingress
spec:
  rules:
  - http:
      paths:
      - path: /hello
        pathType: Prefix
        backend:
          service:
            name: myapp
            port:
              number: 38080

注意,一个集群中最多只应该存在一个默认的 IngressClass,如果有多个 IngressClass 被设置成默认,那么创建 Ingress 时还是得指定 ingressClassName 参数。

深入 Ingress Controller

为了更进一步地了解 Ingress Controller 的工作原理,我们不妨进入 ingress-nginx-controller 容器内部:

# kubectl exec -it ingress-nginx-controller-6c68b88b5d-wdk96 -n ingress-nginx -- bash

在这里我们可以看到 nginx.conf 这个熟悉的身影:

ingress-nginx-controller-6c68b88b5d-wdk96:/etc/nginx$ ls
fastcgi.conf            geoip                   mime.types              nginx.conf              owasp-modsecurity-crs   uwsgi_params
fastcgi.conf.default    koi-utf                 mime.types.default      nginx.conf.default      scgi_params             uwsgi_params.default
fastcgi_params          koi-win                 modsecurity             opentelemetry.toml      scgi_params.default     win-utf
fastcgi_params.default  lua                     modules                 opentracing.json        template

这个文件和普通的 Nginx 配置文件并无二致,查看文件内容可以发现,上面所配置的 Ingress 规则其实都被转换成了 Nginx 规则,此外,我们还发现,Ingress NGINX Controller 是基于 Nginx + Lua 实现的:

ingress-nginx-controller-6c68b88b5d-wdk96:/etc/nginx$ cat nginx.conf

  ## start server _
  server {
    server_name _ ;
    
    listen 80 default_server reuseport backlog=511 ;
    listen [::]:80 default_server reuseport backlog=511 ;
    listen 443 default_server reuseport backlog=511 ssl http2 ;
    listen [::]:443 default_server reuseport backlog=511 ssl http2 ;
    
    location /hello/ {
      
      set $namespace      "default";
      set $ingress_name   "my-ingress";
      set $service_name   "myapp";
      set $service_port   "38080";
      set $location_path  "/hello";
      set $global_rate_limit_exceeding n;
      
      rewrite_by_lua_block {
        lua_ingress.rewrite({
          force_ssl_redirect = false,
          ssl_redirect = true,
          force_no_ssl_redirect = false,
          preserve_trailing_slash = false,
          use_port_in_redirects = false,
          global_throttle = { namespace = "", limit = 0, window_size = 0, key = { }, ignored_cidrs = { } },
        })
        balancer.rewrite()
        plugins.run()
      }
      
      header_filter_by_lua_block {
        lua_ingress.header()
        plugins.run()
      }
      
      body_filter_by_lua_block {
        plugins.run()
      }

      set $proxy_upstream_name "default-myapp-38080";
      
      proxy_pass http://upstream_balancer;
      
    }
  }
  ## end server _
  

其中 upstream_balancer 的定义如下:

  upstream upstream_balancer {
    ### Attention!!!
    #
    # We no longer create "upstream" section for every backend.
    # Backends are handled dynamically using Lua. If you would like to debug
    # and see what backends ingress-nginx has in its memory you can
    # install our kubectl plugin https://kubernetes.github.io/ingress-nginx/kubectl-plugin.
    # Once you have the plugin you can use "kubectl ingress-nginx backends" command to
    # inspect current backends.
    #
    ###
    
    server 0.0.0.1; # placeholder
    
    balancer_by_lua_block {
      balancer.balance()
    }
    
    keepalive 320;
    keepalive_time 1h;
    keepalive_timeout  60s;
    keepalive_requests 10000;
    
  }

通过这里的注释我们了解到,Ingress NGINX Controller 转发的后端地址是动态的,由 Lua 脚本实现,如果想看具体的后端地址,可以安装 ingress-nginx 插件,安装 ingress-nginx 插件最简单的方式是使用 krew 来安装,所以我们先安装 krew,首先下载并解压 krew 的最新版本

# curl -LO https://github.com/kubernetes-sigs/krew/releases/download/v0.4.4/krew-linux_amd64.tar.gz
# tar zxvf krew-linux_amd64.tar.gz

然后运行下面的命令进行安装:

# ./krew-linux_amd64 install krew
Adding "default" plugin index from https://github.com/kubernetes-sigs/krew-index.git.
Updated the local copy of plugin index.
Installing plugin: krew
Installed plugin: krew
\
 | Use this plugin:
 |      kubectl krew
 | Documentation:
 |      https://krew.sigs.k8s.io/
 | Caveats:
 | \
 |  | krew is now installed! To start using kubectl plugins, you need to add
 |  | krew's installation directory to your PATH:
 |  |
 |  |   * macOS/Linux:
 |  |     - Add the following to your ~/.bashrc or ~/.zshrc:
 |  |         export PATH="${KREW_ROOT:-$HOME/.krew}/bin:$PATH"
 |  |     - Restart your shell.
 |  |
 |  |   * Windows: Add %USERPROFILE%\.krew\bin to your PATH environment variable
 |  |
 |  | To list krew commands and to get help, run:
 |  |   $ kubectl krew
 |  | For a full list of available plugins, run:
 |  |   $ kubectl krew search
 |  |
 |  | You can find documentation at
 |  |   https://krew.sigs.k8s.io/docs/user-guide/quickstart/.
 | /
/

根据提示,将 export PATH="${KREW_ROOT:-$HOME/.krew}/bin:$PATH" 添加到 ~/.bashrc 文件中,然后重新打开 Shell,这样 krew 就安装完成了。接下来,使用 kubectl krew install 命令安装 ingress-nginx 插件:

# kubectl krew install ingress-nginx
Updated the local copy of plugin index.
Installing plugin: ingress-nginx
Installed plugin: ingress-nginx
\
 | Use this plugin:
 |      kubectl ingress-nginx
 | Documentation:
 |      https://kubernetes.github.io/ingress-nginx/kubectl-plugin/
/

插件安装之后,使用 kubectl ingress-nginx backends 命令查看 Ingress NGINX Controller 的后端地址信息:

# kubectl ingress-nginx backends -n ingress-nginx
[
  {
    "name": "default-myapp-38080",
    "service": {
      "metadata": {
        "creationTimestamp": null
      },
      "spec": {
        "ports": [
          {
            "name": "http",
            "protocol": "TCP",
            "port": 38080,
            "targetPort": "myapp-port"
          }
        ],
        "selector": {
          "app": "myapp"
        },
        "clusterIP": "10.96.3.215",
        "clusterIPs": [
          "10.96.3.215"
        ],
        "type": "ClusterIP",
        "sessionAffinity": "None",
        "ipFamilies": [
          "IPv4"
        ],
        "ipFamilyPolicy": "SingleStack",
        "internalTrafficPolicy": "Cluster"
      },
      "status": {
        "loadBalancer": {}
      }
    },
    "port": 38080,
    "sslPassthrough": false,
    "endpoints": [
      {
        "address": "100.84.80.88",
        "port": "8080"
      },
      {
        "address": "100.121.213.72",
        "port": "8080"
      },
      {
        "address": "100.121.213.109",
        "port": "8080"
      }
    ],
    "sessionAffinityConfig": {
      "name": "",
      "mode": "",
      "cookieSessionAffinity": {
        "name": ""
      }
    },
    "upstreamHashByConfig": {
      "upstream-hash-by-subset-size": 3
    },
    "noServer": false,
    "trafficShapingPolicy": {
      "weight": 0,
      "weightTotal": 0,
      "header": "",
      "headerValue": "",
      "headerPattern": "",
      "cookie": ""
    }
  },
  {
    "name": "upstream-default-backend",
    "port": 0,
    "sslPassthrough": false,
    "endpoints": [
      {
        "address": "127.0.0.1",
        "port": "8181"
      }
    ],
    "sessionAffinityConfig": {
      "name": "",
      "mode": "",
      "cookieSessionAffinity": {
        "name": ""
      }
    },
    "upstreamHashByConfig": {},
    "noServer": false,
    "trafficShapingPolicy": {
      "weight": 0,
      "weightTotal": 0,
      "header": "",
      "headerValue": "",
      "headerPattern": "",
      "cookie": ""
    }
  }
]

Ingress 类型

根据 Ingress 的使用场景可以将其分成几个不同的类型:

单服务 Ingress

这是最简单的 Ingress 类型,当你只有一个后端 Service 时可以使用它,它不用配置任何路由规则,直接配置一个 defaultBackend 指定后端 Service 即可:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-default-backend
spec:
  defaultBackend:
    service:
      name: myapp
      port:
        number: 38080

使用 kubectl describe ingress 查看该 Ingress 详情:

# kubectl describe ingress ingress-default-backend
Name:             ingress-default-backend
Labels:           <none>
Namespace:        default
Address:          172.31.164.67
Ingress Class:    nginx
Default backend:  myapp:38080 (100.121.213.109:8080,100.121.213.72:8080,100.84.80.88:8080)
Rules:
  Host        Path  Backends
  ----        ----  --------
  *           *     myapp:38080 (100.121.213.109:8080,100.121.213.72:8080,100.84.80.88:8080)
Annotations:  <none>
Events:       <none>

可以看到,无论什么 Host,无论什么 Path,全部路由到 myapp:38080 这个后端服务。

这种 Ingress 和直接使用 NodePortLoadBalancer 类型的 Service 没有区别,不过 defaultBackend 不只是单独使用,也可以和 rules 结合使用,表示兜底路由,当所有的路由规则都不匹配时请求该后端。

多服务 Ingress

这是最常见的一种 Ingress,通过不同的路由规则映射到后端不同的 Service 端口,这种 Ingress 又被称为 Fan Out Ingress,形如其名,它的结构像下面这样成扇形散开:

ingress-simple-fanout.png

下面是多服务 Ingress 的一个示例:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-simple-fanout
  annotations:
    nginx.ingress.kubernetes.io/use-regex: "true"
    nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
  rules:
  - http:
      paths:
      - path: /test(/|$)(.*)
        pathType: ImplementationSpecific
        backend:
          service:
            name: myapp
            port:
              number: 38080
      - path: /test2(/|$)(.*)
        pathType: ImplementationSpecific
        backend:
          service:
            name: hello-actuator
            port:
              number: 8080

使用 kubectl describe ingress 可以查看该 Ingress 的详情:

# kubectl describe ingress ingress-simple-fanout
Name:             ingress-simple-fanout
Labels:           <none>
Namespace:        default
Address:          
Ingress Class:    nginx
Default backend:  <default>
Rules:
  Host        Path  Backends
  ----        ----  --------
  *           
              /test(/|$)(.*)    myapp:38080 (100.121.213.109:8080,100.121.213.72:8080,100.84.80.88:8080)
              /test2(/|$)(.*)   hello-actuator:8080 (100.121.213.108:8080,100.84.80.87:8080)
Annotations:  nginx.ingress.kubernetes.io/rewrite-target: /$2
              nginx.ingress.kubernetes.io/use-regex: true
Events:       <none>

可以看到,当请求路径满足 /test(/|$)(.*) 时,就路由到后端的 myapp:38080 服务,当请求满足 /test2(/|$)(.*) 时,就路由到后端的 hello-actuator:8080 服务。这里有三个参数需要注意:path 表示请求的路径,pathType 表示请求的路径匹配类型,annotations 则是 Ingress Controller 特定的一些注解。

每一个 path 都必须设置 pathTypepathType 有三种取值:

  • Exact:完全匹配,表示当请求的路径和 path 值完全一样时才匹配,比如 path 值为 /foo,请求路径必须为 /foo 才能匹配,如果是 /foo/xxx 或者 /foo/ 都不匹配;
  • Prefix:前缀匹配,表示请求的路径以 path 为前缀时才匹配,比如 path 值为 /foo,请求路径为 /foo/xxx 或者 /foo/ 都可以匹配,但是这里的前缀并完全是字符串前缀匹配,比如请求路径 /foobar 就不能匹配;另外,如果有多个路径都满足匹配规则,那么匹配最严格的那条规则,比如有三个 path,分别是 //aaa/aaa/bbb,当请求路径为 /aaa/bbb 时,匹配的应该是最长的 /aaa/bbb 这个规则;
  • ImplementationSpecific:匹配规则不确定,由 Ingress Controller 来定义;在上面这个例子中,我们就使用了这种匹配类型,我们在 path 中使用了正则表达式,通过正则表达式的分组捕获功能,我们可以在 Ingress NGINX Controller 的 Rewrite annotations 中用来做路由重写。

当我们请求 /test2/actuator/info 这个路径时,默认情况下,Ingress 会将我们的请求转发到后端服务的 /test2/actuator/info 地址,如果希望忽略 /test2 前缀,而转发到后端的 /actuator/info 地址,那就要开启路径重写,Ingress NGINX Controller 提供了一个注解 nginx.ingress.kubernetes.io/rewrite-target 来实现路径重写功能,路径重写一般和 Ingress Path Matching 一起使用,在定义 path 时,先使用正则表达式来匹配路径,比如 /test2(/|$)(.*),然后将 rewrite-target 设置为正则的第二个分组 /$2

虚拟主机 Ingress

Ingress 还支持配置多虚拟主机,将来自不同主机的请求映射到不同的后端服务,如下图所示:

ingress-virtual-host.png

下面是虚拟主机 Ingress 的一个示例:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-virtual-host
spec:
  rules:
  - host: foo.bar.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: myapp
            port:
              number: 38080
  - host: bar.foo.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: hello-actuator
            port:
              number: 8080

这里我们将来自 foo.bar.com 的请求映射到 myapp:38080 服务,将来自 bar.foo.com 的请求映射到 hello-actuator:8080 服务。将这两个域名添加到 /etc/hosts 文件中,使用 curl 验证之:

# curl http://foo.bar.com:26360
Hello Kubernetes bootcamp! | Running on: myapp-b9744c975-9xm5j | v=1

如果不修改 /etc/hosts 文件,也可以通过 curl--resolve 参数手动解析域名:

# curl http://foo.bar.com:26360 --resolve foo.bar.com:26360:172.31.164.40
Hello Kubernetes bootcamp! | Running on: myapp-b9744c975-mb8l2 | v=1

TLS Ingress

我们还可以配置 TLS 证书来加强 Ingress 的安全性,这个证书需要放在一个 Secret 对象中。为了验证这个功能,我们先使用 openssl req 命令生成证书和私钥:

$ openssl req \
  -x509 -sha256 -nodes -days 365 -newkey rsa:2048 \
  -keyout tls.key -out tls.crt \
  -subj "/CN=foo.bar.com/O=foo.bar.com"

这个命令会在当前目录生成两个文件:tls.key 为私钥,tls.crt 为证书,生成的证书中需要指定 CN(Common Name),也被称为 FQDN(Fully Qualified Domain Name),这个一般就是你的域名,对应下面 Ingress 配置中的 host 字段。

然后我们再使用 kubectl create secret tls 命令创建一个 TLS 类型的 Secret,并将这两个文件保存进去:

$ kubectl create secret tls tls-secret --key tls.key --cert tls.crt

创建好的 Secret 如下所示:

# kubectl get secret tls-secret -o yaml
apiVersion: v1
kind: Secret
metadata:
  name: tls-secret
  namespace: default
data:
  tls.crt: LS0t...
  tls.key: LS0t...
type: kubernetes.io/tls

注意,Secret 中必须包含 tls.crttls.key 这两个键。

然后创建 Ingress 时,通过 tls.secretName 参数关联上这个 Secret 名称,就可以开启 TLS 功能了:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: ingress-tls
spec:
  tls:
  - hosts:
      - foo.bar.com
    secretName: tls-secret
  rules:
  - host: foo.bar.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: myapp
            port:
              number: 38080

访问 Ingress 的 HTTPS 端口进行验证:

# curl -k https://foo.bar.com:23476 --resolve foo.bar.com:23476:172.31.164.40
Hello Kubernetes bootcamp! | Running on: myapp-b9744c975-mb8l2 | v=1

使用 TLS Ingress 时有几点要注意:

  • 首先,Ingress 的 TLS 特性被称为 TLS 终止(TLS termination),这意味着从用户到 Ingress 之间的连接是加密的,但是从 Ingress 到 Service 或 Pod 之间的连接仍然是明文的;
  • 其次,Ingress 只支持一个 TLS 端口,当 Ingress 中配置多个主机名时,需要 Ingress Controller 支持 TLS 的 SNI 扩展,Ingress 通过 SNI 来确定使用哪个主机名;
  • 另外,不同 Ingress Controller 支持的 TLS 功能不尽相同,比如 这里 是 Ingress NGINX Controller 关于 TLS/HTTPS 的文档。

其他特性

Ingress NGINX Controller 通过注解和 ConfigMap 还能实现一些其他有用的特性,比如:

更多特性可以参考这里的 注解列表

参考

更多

APISIX Ingress Controller

Kong Ingress Controller

扫描二维码,在手机上阅读!

基于结构化数据的文档问答

利用大模型打造文档问答系统对于个人和企业来说都是一个非常重要的应用场景,也是各大公司争相推出的基于大模型的落地产品之一,同时,在开源领域,文档问答也是非常火热,涌现出了一大批与之相关的开源项目,比如:QuivrPrivateGPTdocument.aiFastGPTDocsGPT 等等。我在 使用 Embedding 技术打造本地知识库助手 这篇笔记中介绍了文档问答的基本原理,通过 OpenAI 的 Embedding 接口实现了一个最简单的本地知识库助手,并在 大模型应用开发框架 LangChain 学习笔记 这篇笔记中通过 LangChain 的 RetrievalQA 再次实现了基于文档的问答,还介绍了四种处理大文档的方法(Stuff Refine MapReduceMapRerank)。

大抵来说,这类文档问答系统基本上都是基于 Embedding 和向量数据库来实现的,首先将文档分割成片段保存在向量库中,然后拿着用户的问题在向量库中检索,检索出来的结果是和用户问题最接近的文档片段,最后再将这些片段和用户问题一起交给大模型进行总结和提炼,从而给出用户问题的答案。在这个过程中,向量数据库是最关键的一环,这也是前一段时间向量数据库火得飞起的原因。

不过,并不是所有的知识库都是以文档的形式存在的,还有很多结构化的知识散落在企业的各种数据源中,数据源可能是 MySQL、Mongo 等数据库,也可能是 CSV、Excel 等表格,还可能是 Neo4j、Nebula 等图谱数据库。如果要针对这些知识进行问答,Embedding 基本上就派不上用场了,所以我们还得想另外的解决方案,这篇文章将针对这种场景做一番粗略的研究。

基本思路

我们知道,几乎每种数据库都提供了对应的查询方法,比如可以使用 SQL 查询 MySQL,使用 VBA 查询 Excel,使用 Cipher 查询 Neo4j 等等。那么很自然的一种想法是,如果能将用户的问题转换为查询语句,就可以先对数据库进行查询得到结果,这和从向量数据库中查询文档是类似的,再将查询出来的结果丢给大模型,就可以回答用户的问题了:

db-qa.png

那么问题来了,如何将用户的问题转换为查询语句呢?毋庸置疑,当然是让大模型来帮忙。

准备数据

首先,我们创建一个测试数据库,然后创建一个简单的学生表,包含学生的姓名、学号、性别等信息:

/*!40101 SET NAMES utf8 */;

CREATE DATABASE IF NOT EXISTS `demo` DEFAULT CHARSET utf8 COLLATE utf8_general_ci;

USE `demo`;

CREATE TABLE IF NOT EXISTS `students`(
   `id` INT UNSIGNED AUTO_INCREMENT,
   `no` VARCHAR(100) NOT NULL,
   `name` VARCHAR(100) NOT NULL,
   `sex` INT NULL,
   `birthday` DATE NULL,
   PRIMARY KEY ( `id` )
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_general_ci;

接着插入 10 条测试数据:

INSERT INTO `students` (`no`, `name`, `sex`, `birthday`) VALUES
('202301030001', '张启文', 1, '2015-04-14'),
('202301030002', '李金玉', 2, '2015-06-28'),
('202301030003', '王海红', 2, '2015-07-01'),
('202301030004', '王可可', 2, '2015-04-03'),
('202301030005', '郑丽英', 2, '2015-10-19'),
('202301030006', '张海华', 1, '2015-01-04'),
('202301030007', '文奇', 1, '2015-11-03'),
('202301030008', '孙然', 1, '2014-12-29'),
('202301030009', '周军', 1, '2015-07-15'),
('202301030010', '罗国华', 1, '2015-08-01');

然后将上面的初始化 SQL 语句放在 init 目录下,通过下面的命令启动 MySQL 数据库:

$ docker run -d -p 3306:3306 --name mysql \
    -v $PWD/init:/docker-entrypoint-initdb.d \
    -e MYSQL_ROOT_PASSWORD=123456 \
    mysql:5.7

将用户问题转为 SQL

接下来,我们尝试一下让大模型将用户问题转换为 SQL 语句。实际上,这被称之为 Text-to-SQL,有很多研究人员对这个课题进行过探讨和研究,Nitarshan Rajkumar 等人在 Evaluating the Text-to-SQL Capabilities of Large Language Models 这篇论文中对各种提示语的效果进行了对比测试,他们发现,当在提示语中使用 CREATE TABLE 来描述数据库表结构时,模型的效果最好。所以我们构造如下的提示语:

from langchain.llms import OpenAI
from langchain.prompts import PromptTemplate

llm = OpenAI(temperature=0.9)

prompt = PromptTemplate.from_template("""根据下面的数据库表结构,生成一条 SQL 查询语句来回答用户的问题:

{schema}

用户问题:{question}
SQL 查询语句:""")

def text_to_sql(schema, question):
    text = prompt.format(schema=schema, question=question)
    response = llm.predict(text)
    return response

这个提示语非常直白,直接将数据库表结构和用户问题丢给大模型,让其生成一条 SQL 查询语句。使用几个简单的问题测试下,发现效果还可以:

schema = "CREATE TABLE ..."
question = "王可可的学号是多少?"
sql = text_to_sql(schema=schema, question=question)
print(sql)

# SELECT no FROM students WHERE name = '王可可';
question = "王可可今年多大?"
sql = text_to_sql(schema=schema, question=question)
print(sql)

# SELECT YEAR(CURRENT_DATE) - YEAR(birthday) FROM students WHERE NAME='王可可';
question = "王可可和孙然谁的年龄大?"
sql = text_to_sql(schema=schema, question=question)
print(sql)

# SELECT NAME, YEAR(CURDATE())-YEAR(birthday) AS age
# FROM students
# WHERE NAME IN ("王可可", "孙然")
# ORDER BY age DESC LIMIT 1;

不过,当我们的字段有特殊含义时,生成的 SQL 语句就不对了,比如这里我们使用 sex=1 表示男生,sex=2 表示女生,但是 ChatGPT 生成 SQL 的时候,认为 sex=0 表示女生:

question = "班上一共有多少个女生?"
sql = text_to_sql(schema=schema, question=question)
print(sql)

# SELECT COUNT(*) FROM students WHERE sex=0;

为了让大模型知道字段的确切含义,我们可以在数据库表结构中给字段加上注释:

schema = """CREATE TABLE IF NOT EXISTS `students`(
   `id` INT UNSIGNED AUTO_INCREMENT,
   `no` VARCHAR(100) NOT NULL,
   `name` VARCHAR(100) NOT NULL,
   `sex` INT NULL COMMENT '1表示男生,2表示女生',
   `birthday` DATE NULL,
   PRIMARY KEY ( `id` )
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE utf8_general_ci;
"""

这样生成的 SQL 语句就没问题了:

question = "班上一共有多少个女生?"
sql = text_to_sql(schema=schema, question=question)
print(sql)

# SELECT COUNT(*) FROM students WHERE sex=2;

根据 Nitarshan Rajkumar 等人的研究,我们还可以对提示语做进一步的优化,比如:

  • 给表结构进行更详细的说明;
  • 在表结构后面加几条具有代表性的示例数据;
  • 增加几个用户问题和对应的 SQL 查询的例子;
  • 使用向量数据库,根据用户问题动态查询相关的 SQL 查询的例子;

执行 SQL

得到 SQL 语句之后,接下来,我们就可以查询数据库了。在 Python 里操作 MySQL 数据库,有两个库经常被人提到:

这两个库的区别在于:PyMySQL 是使用纯 Python 实现的,使用起来简单方便,可以直接通过 pip install PyMySQL 进行安装;mysqlclient 其实就是 Python 3 版本的 MySQLdb,它是基于 C 扩展模块实现的,需要针对不同平台进行编译安装,但正因为此,mysqlclient 的速度非常快,在正式项目中推荐使用它。

这里我们就使用 mysqlclient 来执行 SQL,首先安装它:

$ sudo apt-get install python3-dev default-libmysqlclient-dev build-essential pkg-config
$ pip3 install mysqlclient

然后连接数据库执行 SQL 语句,它的用法和 MySQLdb 几乎完全兼容:

import MySQLdb

def execute_sql(sql):
    result = ''
    db = MySQLdb.connect("192.168.1.44", "root", "123456", "demo", charset='utf8' )
    cursor = db.cursor()
    try:
        cursor.execute(sql)
        results = cursor.fetchall()
        for row in results:
            result += ' '.join(str(x) for x in row) + '\n'
    except:
        print("Error: unable to fetch data")
    db.close()
    return result

注意,大模型生成的 SQL 可能会对数据库造成破坏,所以在生产环境一定要做好安全防护,比如:使用只读的账号,限制返回结果的条数,等等。

回答用户问题

拿到 SQL 语句的执行结果之后,我们就可以再次组织下提示语,让大模型以自然语言的形式来回答用户的问题:

prompt_qa = PromptTemplate.from_template("""根据下面的数据库表结构,SQL 查询语句和结果,以自然语言回答用户的问题:

{schema}

用户问题:{question}
SQL 查询语句:{query}
SQL 查询结果:{result}
回答:""")

def qa(schema, question):
    query = text_to_sql(schema=schema, question=question)
    print(query)
    result = execute_sql(query)
    text = prompt_qa.format(schema=schema, question=question, query=query, result=result)
    response = llm.predict(text)
    return response

测试效果如下:

schema = "CREATE TABLE ..."
question = "王可可的学号是多少?"
answer = qa(schema=schema, question=question)
print(answer)

# 王可可的学号是202301030004。

LangChain

上面的步骤我们也可以使用 LangChain 来实现。

使用 SQLDatabase 获取数据库表结构信息

在 LangChain 的最新版本中,引入了 SQLDatabase 类可以方便地获取数据库表结构信息。我们首先安装 LangChain 的最新版本:

在写这篇博客时,最新版本是 0.0.324,LangChain 的版本更新很快,请随时关注官方文档。

$ pip3 install langchain==0.0.324

然后使用 SQLDatabase.from_uri() 初始化一个 SQLDatabase 实例,由于 SQLDatabase 是基于 SQLAlchemy 实现的,所以参数格式和 SQLAlchemy 的 create_engine 是一致的:

from langchain.utilities import SQLDatabase

db = SQLDatabase.from_uri("mysql+pymysql://root:123456@192.168.1.45:3306/demo?charset=utf8")

然后我们就可以使用 get_table_info() 方法来获取表结构信息:

print(db.get_table_info())

默认情况下该方法会返回数据库中所有表的信息,可以通过 table_names 参数指定只返回某个表的信息:

print(db.get_table_info(table_names=["students"]))

也可以在 SQLDatabase.from_uri() 时通过 include_tables 参数指定:

from langchain.utilities import SQLDatabase

db = SQLDatabase.from_uri("mysql+pymysql://root:123456@192.168.1.45:3306/demo?charset=utf8", include_tables=["students"])

查询结果如下:

CREATE TABLE students (
        id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, 
        no VARCHAR(100) NOT NULL, 
        name VARCHAR(100) NOT NULL, 
        sex INTEGER(11) COMMENT '1表示男生,2表示女生', 
        birthday DATE, 
        PRIMARY KEY (id)
)DEFAULT CHARSET=utf8 ENGINE=InnoDB

/*
3 rows from students table:
id      no      name    sex     birthday
1       202301030001    张启文  1       2015-04-14
2       202301030002    李金玉  2       2015-06-28
3       202301030003    王海红  2       2015-07-01
*/

可以看出 SQLDatabase 不仅查询了表的结构信息,还将表中前三条数据一并返回了,用于组装提示语。

使用 create_sql_query_chain 转换 SQL 语句

接下来我们将用户问题转换为 SQL 语句。LangChain 提供了一个 Chain 来做这个事,这个 Chain 并没有具体的名字,但是我们可以使用 create_sql_query_chain 来创建它:

from langchain.chat_models import ChatOpenAI
from langchain.chains import create_sql_query_chain

chain = create_sql_query_chain(ChatOpenAI(temperature=0), db)

create_sql_query_chain 的第一个参数是大模型,第二个参数是上一节创建的 SQLDatabase,注意这里大模型的参数 temperature=0,因为我们希望大模型生成的 SQL 语句越固定越好,而不是随机变化。

得到 Chain 之后,就可以调用 chain.invoke() 将用户问题转换为 SQL 语句:

response = chain.invoke({"question": "班上一共有多少个女生?"})
print(response)

# SELECT COUNT(*) AS total_female_students
# FROM students
# WHERE sex = 2

其实,create_sql_query_chain 还有第三个参数,用于设置提示语,不设置的话将使用下面的默认提示语:

PROMPT_SUFFIX = """Only use the following tables:
{table_info}

Question: {input}"""

_mysql_prompt = """You are a MySQL expert. Given an input question, first create a syntactically correct MySQL query to run, then look at the results of the query and return the answer to the input question.
Unless the user specifies in the question a specific number of examples to obtain, query for at most {top_k} results using the LIMIT clause as per MySQL. You can order the results to return the most informative data in the database.
Never query for all columns from a table. You must query only the columns that are needed to answer the question. Wrap each column name in backticks (`) to denote them as delimited identifiers.
Pay attention to use only the column names you can see in the tables below. Be careful to not query for columns that do not exist. Also, pay attention to which column is in which table.
Pay attention to use CURDATE() function to get the current date, if the question involves "today".

Use the following format:

Question: Question here
SQLQuery: SQL Query to run
SQLResult: Result of the SQLQuery
Answer: Final answer here

"""

MYSQL_PROMPT = PromptTemplate(
    input_variables=["input", "table_info", "top_k"],
    template=_mysql_prompt + PROMPT_SUFFIX,
)

执行 SQL 语句并回答用户问题

得到 SQL 语句之后,我们就可以通过 SQLDatabase 运行它:

result = db.run(response)
print(result)

# [(4,)]

然后再重新组织提示语,让大模型以自然语言的形式对用户问题进行回答,跟上面类似,此处略过。

使用 SQLDatabaseChain 实现数据库问答

不过 LangChain 提供了更方便的方式实现数据库问答,那就是 SQLDatabaseChain,可以将上面几个步骤合而为一。不过 SQLDatabaseChain 目前还处于实验阶段,我们需要先安装 langchain_experimental

$ pip3 install langchain_experimental==0.0.32

然后就可以使用 SQLDatabaseChain 来回答用户问题了:

from langchain.utilities import SQLDatabase
from langchain.llms import OpenAI
from langchain_experimental.sql import SQLDatabaseChain

db = SQLDatabase.from_uri("mysql+pymysql://root:123456@192.168.1.45:3306/demo?charset=utf8")
llm = OpenAI(temperature=0, verbose=True)
db_chain = SQLDatabaseChain.from_llm(llm, db, verbose=True)

response = db_chain.run("班上一共有多少个女生?")
print(response)

我们通过 verbose=True 参数让 SQLDatabaseChain 输出执行的详细过程,结果如下:

> Entering new SQLDatabaseChain chain...
班上一共有多少个女生?
SQLQuery:SELECT COUNT(*) FROM students WHERE sex = 2;
SQLResult: [(4,)]
Answer:班上一共有4个女生。
> Finished chain.
班上一共有4个女生。

注意:SQLDatabaseChain 会一次性查询出数据库中所有的表结构,然后丢给大模型生成 SQL,当数据库中表较多时,生成效果可能并不好,这时最好手工指定使用哪些表,再生成 SQL,或者使用 SQLDatabaseSequentialChain,它第一步会让大模型确定该使用哪些表,然后再调用 SQLDatabaseChain

使用 SQL Agent 实现数据库问答

大模型应用开发框架 LangChain 学习笔记(二) 这篇笔记中,我们学习了 LangChain 的 Agent 功能,借助 ReAct 提示工程或 OpenAI 的 Function Calling 技术,可以让大模型具有推理和使用外部工具的能力。很显然,如果我们将数据库相关的操作都定义成一个个的工具,那么通过 LangChain Agent 应该也可以实现数据库问答功能。

LangChain 将数据库相关的操作封装在 SQLDatabaseToolkit 工具集中,我们可以直接使用:

from langchain.agents.agent_toolkits import SQLDatabaseToolkit
from langchain.sql_database import SQLDatabase
from langchain.llms.openai import OpenAI

db = SQLDatabase.from_uri("mysql+pymysql://root:123456@192.168.1.45:3306/demo?charset=utf8")
toolkit = SQLDatabaseToolkit(db=db, llm=OpenAI(temperature=0))

这个工具集中实际上包含了四个工具:

工具名工具类工具说明
sql_db_list_tablesListSQLDatabaseTool查询数据库中所有表名
sql_db_schemaInfoSQLDatabaseTool根据表名查询表结构信息和示例数据
sql_db_queryQuerySQLDataBaseTool执行 SQL 返回执行结果
sql_db_query_checkerQuerySQLCheckerTool使用大模型分析 SQL 语句是否正确

另外,LangChain 还提供了 create_sql_agent 方法用于快速创建一个用于处理数据库的 Agent:

from langchain.agents import create_sql_agent
from langchain.agents.agent_types import AgentType

agent_executor = create_sql_agent(
    llm=OpenAI(temperature=0),
    toolkit=toolkit,
    verbose=True,
    agent_type=AgentType.ZERO_SHOT_REACT_DESCRIPTION,
)

其中 agent_type 只能选 ZERO_SHOT_REACT_DESCRIPTIONOPENAI_FUNCTIONS 这两种,实际上就对应着 ZeroShotAgentOpenAIFunctionsAgent,在使用上和其他的 Agent 并无二致:

response = agent_executor.run("班上一共有多少个女生?")
print(response)

执行结果如下:

> Entering new AgentExecutor chain...
Thought: I should query the database to get the answer.
Action: sql_db_list_tables
Action Input: ""
Observation: students
Thought: I should query the schema of the students table.
Action: sql_db_schema
Action Input: "students"
Observation: 
CREATE TABLE students (
        id INTEGER(10) UNSIGNED NOT NULL AUTO_INCREMENT, 
        no VARCHAR(100) NOT NULL, 
        name VARCHAR(100) NOT NULL, 
        sex INTEGER(11) COMMENT '1表示男生,2表示女生', 
        birthday DATE, 
        PRIMARY KEY (id)
)DEFAULT CHARSET=utf8 ENGINE=InnoDB

/*
3 rows from students table:
id      no      name    sex     birthday
1       202301030001    张启文  1       2015-04-14
2       202301030002    李金玉  2       2015-06-28
3       202301030003    王海红  2       2015-07-01
*/
Thought: I should query the database to get the number of female students.
Action: sql_db_query
Action Input: SELECT COUNT(*) FROM students WHERE sex = 2
Observation: [(4,)]
Thought: I now know the final answer.
Final Answer: 班上一共有4个女生。

> Finished chain.
班上一共有4个女生。

可以看出整个执行过程非常流畅,首先获取数据库中的表,然后查询表结构,接着生成 SQL 语句并执行,最后得到用户问题的答案。

使用 SQL Agent 比 SQLDatabaseChain 要灵活的多,我们不仅可以实现数据库问答,还可以实现一些其他功能,比如 SQL 生成,SQL 校验,SQL 解释和优化,生成数据库描述,等等,我们还可以根据需要在工具集中添加自己的工具,扩展出更丰富的功能。

LangChain 的 这篇文档中 就给出了两个拓展工具集的例子,我觉得很有参考意义:

  • Including dynamic few-shot examples:这个例子将一些用户问题和对应的 SQL 示例存储到向量数据库中,然后创建一个额外的名为 sql_get_similar_examples 的工具用于从向量库中获取类似示例,并将提示语修改为:先从向量库中查找类似的示例,判断示例能否构造出回答用户问题的 SQL 语句,如果能,直接通过示例构造出 SQL 语句,如果不能,则通过查询数据库的表结构来构造;
  • Finding and correcting misspellings for proper nouns:这也是一个很实用的示例,用户在提问时往往会输入一些错别字,特别是人物名称、公司名称或地址信息等专有名词,比如将 张晓红今年多大? 写成了 张小红今年多大?,这时直接搜索数据库肯定是搜不出结果的。在这个例子中,首先将数据库中所有艺人名称和专辑名称存储到向量数据库中,然后创建了一个额外的名为 name_search 的工具用于从向量库中获取近似的名称,并将提示语修改为:如果用户问题设计到专有名词,首先搜索向量库判断名称的拼写是否有误,如果拼写有误,要使用正确的名称构造 SQL 语句来回答用户的问题。

一些开源项目

目前市面上已经诞生了大量基于结构化数据的问答产品,比如 酷表ExcelSheet+Julius AI 等,它们通过聊天的方式来操控 Excel 或 Google Sheet 表格,还有 AI QueryAI2sql 等,它们将自然语言转化为可以执行的 SQL 语句,让所有数据库小白也可以做数据分析。

在开源社区,类似的项目也是百花齐放,比如 sql-translatortextSQLsqlchatChat2DBDB-GPT 等等,其中热度最高的当属 Chat2DB 和 DB-GPT 这两个开源项目。

Chat2DB

Chat2DB 是一款智能的数据库客户端软件,和 Navicat、DBeaver 相比,Chat2DB 集成了 AIGC 的能力,能够将自然语言转换成 SQL,也可以将 SQL 翻译成自然语言,或对 SQL 提出优化建议;此外,Chat2DB 还集成了报表能力,用户可以用对话的形式进行数据统计和分析。

Chat2DB 提供了 Windows、Mac、Linux 等平台的安装包,也支持以 Web 形式进行部署,我们直接通过官方镜像安装:

$ docker run --name=chat2db -ti -p 10824:10824 chat2db/chat2db:latest

启动成功后,访问 http://localhost:10824 会进入 Chat2DB 的登录页面,默认的用户名和密码是 chat2db/chat2db,登录成功后,添加数据库连接,然后可以创建新的表,查看表结构,查看表中数据,等等,这和传统的数据库客户端软件并无二致:

chat2db-console.png

和传统的数据库客户端软件相比,Chat2DB 最重要的一点区别在于,用户可以在控制台中输入自然语言,比如像下面这样,输入 查询王可可的学号 并回车,这时会自动生成 SQL 语句,点击执行按钮就可以得到结果:

chat2db-generate-sql-select.png

通过这种方式来管理数据库让人感觉很自然,即使数据库小白也能对数据库进行各种操作,比如要创建一个表:

chat2db-generate-sql-create-table.png

插入一些测试数据:

chat2db-generate-sql-insert.png

遇到不懂的 SQL 语句,用户还可以对 SQL 语句进行解释和优化,整个体验可以说是非常流畅,另外,Chat2DB 还支持通过自然语言的方式生成报表,比如柱状图、折线图、饼图等,便于用户进行数据统计和分析:

chat2db-dashboard.png

默认情况下,Chat2DB 使用的是 Chat2DB AI 接口,关注官方公众号后就可以免费使用,我们也可以切换成 OpenAI 或 AzureAI 接口,或使用自己部署的大模型接口,具体内容请参考官方提供的 ChatGLM-6Bsqlcoder 的部署方法。

DB-GPT

DB-GPT 是一款基于知识库的问答产品,它同时支持结构化和非结构化数据的问答,支持生成报表,还支持自定义插件,在交互形式上和 ChatGPT 类似。它的一大特点是支持海量的模型管理,包括开源模型和 API 接口,并支持模型的自动化微调。

DB-GPT 的侧重点在于私有化,它强调数据的隐私安全,可以做到整个系统都不依赖于外部环境,完全避免了数据泄露的风险,是一款真正意义上的本地知识库问答产品。它集成了常见的开源大模型和向量数据库,因此,在部署上复杂一点,而且对硬件的要求也要苛刻一点。不过对于哪些没有条件部署大模型的用户来说,DB-GPT 也支持直接 使用 OpenAI 或 Bard 等接口

DB-GPT 支持 从源码安装从 Docker 镜像安装,不过官方提供的 Docker 镜像缺少 openai 等依赖,需要我们手工安装,所以不建议直接启动,而是先通过 bash 进入容器做一些准备工作:

$ docker run --rm -ti -p 5000:5000 eosphorosai/dbgpt:latest bash

安装 openai 依赖:

# pip3 install openai

然后设置一些环境变量,让 DB-GPT 使用 OpenAI 的 Completions 和 Embedding 接口:

# export LLM_MODEL=chatgpt_proxyllm
# export PROXY_SERVER_URL=https://api.openai.com/v1/chat/completions
# export PROXY_API_KEY=sk-xx
# export EMBEDDING_MODEL=proxy_openai
# export proxy_openai_proxy_server_url=https://api.openai.com/v1
# export proxy_openai_proxy_api_key=sk-xxx

如果由于网络原因导致 OpenAI 接口无法访问,还需要配置代理(注意先安装 pysocks 依赖):

# pip3 install pysocks
# export https_proxy=socks5://192.168.1.45:7890
# export http_proxy=socks5://192.168.1.45:7890

一切准备就绪后,启动 DB-GPT server:

# python3 pilot/server/dbgpt_server.py

等待服务启动成功,访问 http://localhost:5000/ 即可进入 DB-GPT 的首页:

dbgpt-home.png

DB-GPT 支持几种类型的聊天功能:

  • Chat Data
  • Chat Excel
  • Chat DB
  • Chat Knowledge
  • Dashboard
  • Agent Chat

Chat DB & Chat Data & Dashboard

Chat DB、Chat Data 和 Dashboard 这三个功能都是基于数据库的问答,要使用它们,首先需要在 数据库管理 页面添加数据库。Chat DB 会根据数据库和表的结构信息帮助用户编写 SQL 语句:

dbgpt-chat-db.png

Chat Data 不仅会生成 SQL 语句,还会自动执行并得到结果:

dbgpt-chat-data.png

Dashboard 则比 Chat Data 更进一步,它会生成 SQL 语句,执行得到结果,并生成相应的图表:

dbgpt-chat-dashboard.png

Chat Excel

Chat Excel 功能依赖 openpyxl 库,需要提前安装:

# pip3 install openpyxl

然后就可以上传 Excel 文件,对其进行分析,并回答用户问题:

dbgpt-chat-excel.png

其他功能

DB-GPT 除了支持结构化数据的问答,也支持非结构化数据的问答,Chat Knowledge 实现的就是这样的功能。要使用它,首先需要在 知识库管理 页面添加知识,DB-GPT 支持从文本,URL 或各种文档中导入:

dbgpt-chat-kb.png

然后在 Chat Knowledge 页面就可以选择知识库进行问答了。

DB-GPT 还支持插件功能,你可以从 DB-GPT-Plugins 下载插件,也可以 编写自己的插件并上传,而且 DB-GPT 兼容 Auto-GPT 的插件 接口,原则上,所有的 Auto-GPT 插件都可以在这里使用:

dbgpt-chat-plugins.png

然后在 Agent Chat 页面,就可以像 ChatGPT Plus 一样,选择插件进行问答了。

另外,DB-GPT 的模型管理功能也很强大,不仅支持像 OpenAI 或 Bard 这样的大模型代理接口,还集成了大量的开源大模型,而且在 DB-GPT-Hub 项目中还提供了大量的数据集、工具和文档,让我们可以对这些大模型进行微调,实现更强大的 Text-to-SQL 能力。

参考

更多

基于其他结构化数据源的文档问答

Neo4j

Elasticsearch

CSV

Excel

基于半结构化和多模数据源的文档问答

学习 LCEL

在 LangChain 中,我们还可以通过 LCEL(LangChain Expression Language) 来简化 Chain 的创建,比如对数据库进行问答,官方有一个示例,可以用下面这样的管道式语法来写:

full_chain = (
    RunnablePassthrough.assign(query=sql_response)
    | RunnablePassthrough.assign(
        schema=get_schema,
        response=lambda x: db.run(x["query"]),
    )
    | prompt_response
    | model
)
扫描二维码,在手机上阅读!