文档问答系统后端部署手册

本手册为针对Linux操作系统 + GPU服务器上的部署手册。

GPU服务器上部署本程序会踩很多坑,本人就曾经一个弄两天还没部署成功,最后成功后,没想到十几天后因为服务器断电坏了,所以所有东西需要重新部署,又把所有坑踩了一遍。

这次我吸取教训,部署的同时写个部署手册,后续系统升级的同学请继续维护本手册,保证其他人能正常部署。

代码层:修改数据库配置和大模型API配置

这一步经常忘记。

chatdoc/config.py中:

  1. 需要修改数据库为自行的数据库。
  2. 修改ApiKey为自己的。

代码层:修改星火Embedding的源码

如果不使用星火Embedding可以跳过,但需要修改Embedding层为自己的Embedding模型

原因简单来说就是,截止到我开发这个系统的时候,langchain中没有集成星火的Embedding新版,所以需要根据官网Api修改一下源码。

第二个原因是,免费版Api有QPS限制,需要time.sleep一下,有点烦。

代码详细见下方的“其他问题”。

安装Anaconda

如果服务器装了Anaconda可以跳过。

  1. 下载anaconda安装包,上传到服务器上。
  2. 执行chmod +x Anaconda3-2024.06-1-Linux-x86_64.sh ,然后执行这个sh脚本。
  3. 根据提示安装,流程大概就是读个免责声明之类的,疯狂Enter即可。
  4. 注意最后有一个是否设置环境变量的需要设置为yes。否则就要去家目录的.bashrc最后加一句: export PATH=/home/anaconda3/bin:$PATH
  5. 新建一个Shell,即可看到conda启动了。

创建虚拟环境和安装Pytorch

第一步,创建Python环境,注意选的是python=3.10的版本


conda create -n chatdoc python=3.10

第二步,配置清华源:(忘记配可能会经常timeout的异常)

python -m pip install --upgrade pip
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

第三步,拉GitHub代码或者自行上传后端代码。然后安装依赖。

安装依赖:

pip install -r requirements.txt

第四步,还没完,继续安装一些依赖。

查询自己的服务器架构和CUDA版本。
```bash
nvcc -V

uname -a

安装zh_core_web_sm-3.7.0-py3-none-any:(我放到pkg文件夹里面了)

  • 首先,找到最新版本的下载:https://github.com/explosion/spacy-models/releases?q=zh_core_web_sm&expanded=true
  • 然后,使用pip install ./pkg/zh_core_web_sm-3.7.0-py3-none-any.whl

安装paddlepaddle:(自行从官网寻找合适的版本:https://www.paddlepaddle.org.cn/install/quick?docurl=/documentation/docs/zh/install/pip/linux-pip.html)

python -m pip install paddlepaddle-gpu==2.6.1.post120 -f https://www.paddlepaddle.org.cn/whl/linux/mkl/avx/stable.html

安装适合的pytorch:(国内镜像:https://mirrors.aliyun.com/pytorch-wheels/cu121/?spm=a2c6h.25603864.0.0.672f6223S2zQfR)

pip install torch-2.2.1+cu121-cp310-cp310-linux_x86_64.whl

安装tmux

tmux用于创建会话,保证服务器关闭了服务仍然能执行。

创建新会话:

tmux new -s chatdoc

连接上新会话:

tmux attach -t chatdoc

退出会话:ctrl+b d

设置CUDA运行卡号

GPU资源珍贵,往往需要预约才能够使用。可以使用如export CUDA_VISIBLE_DEVICES=2,3设置要运行的GPU卡号。

NAT(非必要)

如有必要,请上传Linux版本的frp,并配置frpc,服务器仅充当frp的客户端,不对外暴露端口。

frpc.toml

serverAddr = "你的frp服务器IP地址"
serverPort = frp端口,默认是7000

[[proxies]]
name = "cnsoftbei-chatdoc-frp"
type = "http"
localPort = 本地服务端口,这里是5005
customDomains = ["你的frp服务器IP地址"]

PS:同样可以跑一个tmux承载frp服务。

启动:./frpc -c frpc.toml

其它常见部署问题

星火Embedding错误(免费版QPS限制导致 + 截止我开发的时候langchain的api没更新到星火embedding最新版)

首先使用find ~ -name sparkllm.py找到星火embedding的文件。

直接对源码进行改动:

import base64
import hashlib
import hmac
import json
import logging
import time
from datetime import datetime
from time import mktime
from typing import Any, Dict, List, Optional
from urllib.parse import urlencode
from wsgiref.handlers import format_date_time

import numpy as np
import requests
from langchain_core.embeddings import Embeddings
from langchain_core.pydantic_v1 import BaseModel, SecretStr, root_validator
from langchain_core.utils import convert_to_secret_str, get_from_dict_or_env
from numpy import ndarray

# Used for document and knowledge embedding
EMBEDDING_P_API_URL: str = "https://cn-huabei-1.xf-yun.com/v1/private/sa8a05c27"
# Used for user questions embedding
EMBEDDING_Q_API_URL: str = "https://cn-huabei-1.xf-yun.com/v1/private/s50d55a16"

# 对源码进行改动
NEW_EMBEDDING_API_URL: str = "https://emb-cn-huabei-1.xf-yun.com/"

# SparkLLMTextEmbeddings is an embedding model provided by iFLYTEK Co., Ltd.. (https://iflytek.com/en/).

# Official Website: https://www.xfyun.cn/doc/spark/Embedding_new_api.html
# Developers need to create an application in the console first, use the appid, APIKey,
# and APISecret provided in the application for authentication,
# and generate an authentication URL for handshake.
# You can get one by registering at https://console.xfyun.cn/services/bm3.
# SparkLLMTextEmbeddings support 2K token window and preduces vectors with
# 2560 dimensions.

logger = logging.getLogger(__name__)


class Url:
    def __init__(self, host: str, path: str, schema: str) -> None:
        self.host = host
        self.path = path
        self.schema = schema
        pass


class SparkLLMTextEmbeddings(BaseModel, Embeddings):
    """SparkLLM Text Embedding models."""

    spark_app_id: SecretStr
    spark_api_key: SecretStr
    spark_api_secret: SecretStr

    @root_validator(allow_reuse=True)
    def validate_environment(cls, values: Dict) -> Dict:
        """Validate that auth token exists in environment."""
        cls.spark_app_id = convert_to_secret_str(
            get_from_dict_or_env(values, "spark_app_id", "SPARK_APP_ID")
        )
        cls.spark_api_key = convert_to_secret_str(
            get_from_dict_or_env(values, "spark_api_key", "SPARK_API_KEY")
        )
        cls.spark_api_secret = convert_to_secret_str(
            get_from_dict_or_env(values, "spark_api_secret", "SPARK_API_SECRET")
        )
        return values

    def _embed(self, texts: List[str], host: str,type:str) -> Optional[List[List[float]]]:
        url = self._assemble_ws_auth_url(
            request_url=host,
            method="POST",
            api_key=self.spark_api_key.get_secret_value(),
            api_secret=self.spark_api_secret.get_secret_value(),
        )
        embed_result: list = []
        for text in texts:
            query_context = {"messages": [{"content": text, "role": "user"}]}
            content = self._get_body(
                self.spark_app_id.get_secret_value(), query_context,type=type
            )
            response = requests.post(
                url, json=content, headers={"content-type": "application/json"}
            ).text
            res_arr = self._parser_message(response)
            if res_arr is not None:
                embed_result.append(res_arr.tolist())
            else:
                embed_result.append(None)
            # 如果要用星火Embedding,得减速,有QPS限制
            time.sleep(0.5)
        return embed_result

    def embed_documents(self, texts: List[str]) -> Optional[List[List[float]]]:  # type: ignore[override]
        """Public method to get embeddings for a list of documents.

        Args:
            texts: The list of texts to embed.

        Returns:
            A list of embeddings, one for each text, or None if an error occurs.
        """
        return self._embed(texts, NEW_EMBEDDING_API_URL,"para")

    def embed_query(self, text: str) -> Optional[List[float]]:  # type: ignore[override]
        """Public method to get embedding for a single query text.

        Args:
            text: The text to embed.

        Returns:
            Embeddings for the text, or None if an error occurs.
        """
        result = self._embed([text], NEW_EMBEDDING_API_URL,"query")
        return result[0] if result is not None else None

    @staticmethod
    def _assemble_ws_auth_url(
        request_url: str, method: str = "GET", api_key: str = "", api_secret: str = ""
    ) -> str:
        u = SparkLLMTextEmbeddings._parse_url(request_url)
        host = u.host
        path = u.path
        now = datetime.now()
        date = format_date_time(mktime(now.timetuple()))
        signature_origin = "host: {}\ndate: {}\n{} {} HTTP/1.1".format(
            host, date, method, path
        )
        signature_sha = hmac.new(
            api_secret.encode("utf-8"),
            signature_origin.encode("utf-8"),
            digestmod=hashlib.sha256,
        ).digest()
        signature_sha_str = base64.b64encode(signature_sha).decode(encoding="utf-8")
        authorization_origin = (
            'api_key="%s", algorithm="%s", headers="%s", signature="%s"'
            % (api_key, "hmac-sha256", "host date request-line", signature_sha_str)
        )
        authorization = base64.b64encode(authorization_origin.encode("utf-8")).decode(
            encoding="utf-8"
        )
        values = {"host": host, "date": date, "authorization": authorization}

        return request_url + "?" + urlencode(values)

    @staticmethod
    def _parse_url(request_url: str) -> Url:
        stidx = request_url.index("://")
        host = request_url[stidx + 3 :]
        schema = request_url[: stidx + 3]
        edidx = host.index("/")
        if edidx <= 0:
            raise AssembleHeaderException("invalid request url:" + request_url)
        path = host[edidx:]
        host = host[:edidx]
        u = Url(host, path, schema)
        return u

    @staticmethod
    def _get_body(appid: str, text: dict, type: str) -> Dict[str, Any]:
        body = {
            "header": {"app_id": appid, "uid": "39769795890", "status": 3},
            "parameter": {"emb": {"feature": {"encoding": "utf8","compress":"raw","format":"plain"},"domain": type}},
            "payload": {
                "messages": {
                    "encoding": "utf8",
                    "compress": "raw",
                    "format": "json",
                    "status": 3,
                    "text": base64.b64encode(json.dumps(text).encode("utf-8")).decode()
                }
            },
        }
        return body

    @staticmethod
    def _parser_message(
        message: str,
    ) -> Optional[ndarray]:
        data = json.loads(message)
        code = data["header"]["code"]
        if code != 0:
            logger.warning(f"Request error: {code}, {data}")
            return None
        else:
            text_base = data["payload"]["feature"]["text"]
            text_data = base64.b64decode(text_base)
            dt = np.dtype(np.float32)
            dt = dt.newbyteorder("<")
            text = np.frombuffer(text_data, dtype=dt)
            if len(text) > 2560:
                array = text[:2560]
            else:
                array = text
            return array


class AssembleHeaderException(Exception):
    """Exception raised for errors in the header assembly."""

    def __init__(self, msg: str) -> None:
        self.message = msg

(已解决)Paddle报错1:找不到libcudnn_ops_infer.so

报错信息:

Could not load library libcudnn_ops_infer.so.8. Error: libcudnn_ops_infer.so.8: cannot open shared object file: No such file or directory
Please make sure libcudnn_ops_infer.so.8 is in your library path!

解决方案:

  1. 去cudnn官网找到合适的cudnn版本,下载,然后传到服务器。
  2. 设置环境变量,类似这个,将下载的cudnn包中的lib加上去:export LD_LIBRARY_PATH=.:/usr/local/cuda-10.2/lib64:/home/username/demo/cuda/lib64

Paddle报错2: (PreconditionNotMet) The third-party dynamic library (libnccl.so)

报错信息:

RuntimeError: (PreconditionNotMet) The third-party dynamic library (libnccl.so) that Paddle depends on is not configured correctly. (error code is libnccl.so: cannot open shared object file: No such file or directory)
  Suggestions:
  1. Check if the third-party dynamic library (e.g. CUDA, CUDNN) is installed correctly and its version is matched with paddlepaddle you installed.
  2. Configure third-party dynamic library environment variables as follows:
  - Linux: set LD_LIBRARY_PATH by `export LD_LIBRARY_PATH=...`
  - Windows: set PATH by `set PATH=XXX; (at ../paddle/phi/backends/dynload/dynamic_loader.cc:312)

解决:暂无解决方案,应该是CUDA版本的问题。换个服务器试试,我换了一下就好了。

PS:如果出现下面报错信息,需要更新libstdcxx-ng依赖:

ImportError: /home/xiaoyongjie/anaconda3/envs/chatdoc/bin/../lib/libstdc++.so.6: version `GLIBCXX_3.4.30' not found (required by /home/xiaoyongjie/anaconda3/envs/chatdoc/lib/python3.10/site-packages/paddle/base/libpaddle.so)

命令:

conda install -c conda-forge libstdcxx-ng

上传文档报错

报错信息:

Error: (PreconditionNotMet) Cannot load cudnn shared library. Cannot invoke method cudnnGetVersion.
  [Hint: cudnn_dso_handle should not be null.] (at ../paddle/phi/backends/dynload/cudnn.cc:60)

也是paddle的问题,暂时想不到解决方案......换个服务器试试。

docker compose中MySQL密码失效的解决方法(这里用不到,但是因为以后可能用docker部署,还是写上)

首先拷贝容器的my.cnf到本地,修改之:将配置文件从容器映射出来,并在[mysqld]下面加上 skip-grant-tables。

#将容器中的文件拷贝出来
sudo docker cp 容器ID:/etc/mysql/my.cnf ~/my.cnf
#将容器中的文件拷贝回去
sudo docker cp ~/my.cnf  容器ID:/etc/mysql/

docker restart 容器ID重启容器。

使用docker exec -it 容器ID mysql进入重启。

重新设置MySQL密码:

USE mysql;
UPDATE user SET authentication_string=PASSWORD('new_password') WHERE User='root';
FLUSH PRIVILEGES;

退出容器,重启容器即可。

问题原因(网上的分析)

这个问题的原因在于MySQL镜像的启动逻辑。MySQL镜像在启动时,会检查是否已经存在数据目录/var/lib/mysql,如果不存在则会执行初始化操作,包括创建并启动MySQL服务。在这个过程中,MySQL会自动生成一个随机密码,并将该密码输出到容器的日志中。

所以,当我们在docker-compose.yml中设置了MYSQL_ROOT_PASSWORD环境变量后,MySQL容器启动时会先使用这个密码进行初始化,然后才会被自动生成的随机密码替代。因此导致我们设置的密码并没有生效。

明明MySQL密码配置的没问题,但是却一直连不上数据库(这里用不到,但是因为以后可能用docker部署,还是写上)

如果确定自己的账号密码没问题,试试将yaml中的数据库账号密码和URL等用双引号括起来。一般来说就好了。