跳到主要内容

115 篇博文 含有标签「开发工具与环境」

开发工具和环境配置相关文章

查看所有标签

Linux 使用 tar 命令进行迁移

· 阅读需 2 分钟
素明诚
Full stack development

以迁移 MongoDB 的数据为例。

停止当前机器上运行的 MongoDB 服务,以确保数据库文件处于一致状态。

打包

本次迁移的是 mongo/ 的卷文件到另一台机器上

使用 tar 命令将 mongo 目录打包成一个归档文件。tar 命令可以保留文件权限和所有者信息。

tar -czvf mongo.tar.gz mongo/

这会将 mongo 目录打包成一个名为 mongo.tar.gz 的归档文件。

校验

使用 md5sumsha256sum 命令计算归档文件的校验和。例如:

md5sum mongo.tar.gz > mongo.tar.gz.md5

这会计算 mongo.tar.gz 文件的 MD5 校验和,并将其保存到 mongo.tar.gz.md5 文件中。

使用安全的文件传输工具将归档文件 mongo.tar.gz 和校验和文件 mongo.tar.gz.md5 传输到目标机器。例如,你可以使用 scprsync 等工具进行传输。

scp mongo.tar.gz mongo.tar.gz.md5 user@target_machine:/path/to/destination/

user 替换为目标机器的用户名,target_machine 替换为目标机器的主机名或 IP,/path/to/destination/ 替换为目标机器上的目标目录。

完整性校验

在目标机器上,使用 md5sumsha256sum 命令验证接收到的归档文件的完整性。

md5sum -c mongo.tar.gz.md5

这会计算接收到的 mongo.tar.gz 文件的 MD5 校验和,并与 mongo.tar.gz.md5 文件中的校验和进行比较。如果校验和匹配,则说明文件在传输过程中没有被修改。

1404a69a7ed73f6e9d229366031c54ed## 解压

如果校验和匹配,使用 tar 命令将归档文件解压缩到目标目录。

tar -xzvf mongo.tar.gz

这会将 mongo.tar.gz 文件解压缩,并将 mongo 目录及其内容提取到当前目录。

通过以上步骤,你可以确保 mongo 目录在传输过程中的完整性,并将其准确地复制到目标机器上。

Docker 更换镜像源阿里科大网易腾讯中科大微软官方

· 阅读需 2 分钟
素明诚
Full stack development

编辑 Docker 配置文件: 打开或创建 /etc/docker/daemon.json 文件

{
"registry-mirrors": [
"https://registry.docker-cn.com",
"http://hub-mirror.c.163.com",
"https://docker.mirrors.ustc.edu.cn",
"https://dockerhub.azk8s.cn",
"https://mirror.ccs.tencentyun.com",
"https://registry.cn-hangzhou.aliyuncs.com",
"https://docker.mirrors.ustc.edu.cn"
]
}

重启 Docker 服务: 为使配置生效,请执行以下命令:

sudo systemctl daemon-reload
sudo systemctl restart docker

原地址

Docker 官方镜像加速器(中国区)

  • 地址: https://registry.docker-cn.com

网易云镜像加速器

  • 地址: http://hub-mirror.c.163.com

科大讯飞开源镜像加速器

  • 地址: https://docker.mirrors.ustc.edu.cn

Azure 中国镜像加速器

  • 地址: https://dockerhub.azk8s.cn

腾讯云公共镜像库

  • 地址: https://mirror.ccs.tencentyun.com

阿里云公共镜像加速器

  • 地址: https://registry.cn-hangzhou.aliyuncs.com

中国科学技术大学镜像加速器

  • 地址: https://docker.mirrors.ustc.edu.cn

2024 年 8 月 2 日 09:14:06 更新

如果上面的 G 了,可以试试下面这些

{
"registry-mirrors": [
"https://hub.uuuadc.top",
"https://docker.anyhub.us.kg",
"https://dockerhub.jobcher.com",
"https://dockerhub.icu",
"https://docker.ckyl.me",
"https://docker.awsl9527.cn"
]
}

2024 年 8 月 12 日 15:32:23 更新

镜像源失效的问题,通常有以下几种长期解决方案

使用阿里云+GitHub 构建镜像

这个网上有很多教程,就细说了

本机配置代理 (推荐)

可以封装一下 clash,容器启动,方便管理

搭建私有容器镜像仓库(如 Harbor)

Harbor 是一个开源的企业级容器镜像仓库,提供了镜像管理、访问控制、安全扫描等功能。搭建私有仓库可以方便团队内部的镜像下载和同步,同时也需要配置相应的代理以加速访问。

FastGPT47 chatglm3-6b m3e 本地部署

· 阅读需 8 分钟
素明诚
Full stack development

安装 Docker 和 docker-compose

Windows 可以使用 wsl

# 安装 Docker
curl -fsSL https://get.docker.com | bash -s docker --mirror Aliyun
systemctl enable --now docker
# 安装 docker-compose
curl -L https://github.com/docker/compose/releases/download/v2.20.3/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
# 验证安装
docker -v
docker-compose -v

安装并启动

docker-compose.yml 和 config.json 这两个文件很重要,安装后不要删掉~

mkdir fastgpt
cd fastgpt
curl -O https://raw.githubusercontent.com/labring/FastGPT/main/files/deploy/fastgpt/docker-compose.yml
curl -O https://raw.githubusercontent.com/labring/FastGPT/main/projects/app/data/config.json

注意启动的时候要在 fastgpt 这个目录

# 启动容器
docker-compose up -d
# 等待10s,OneAPI第一次总是要重启几次才能连上Mysql
sleep 10
# 重启一次oneapi(由于OneAPI的默认Key有点问题,不重启的话会提示找不到渠道,临时手动重启一次解决,等待作者修复)
docker restart oneapi

5b6581830daeef63ab9688aa590e002a

安装成功后可以看到红框里的容器,m3e 后续再说

服务说明

OneAPI:提供标准的 API 格式,后续 M3E 和 GLM 都要对接到这里面

FastGPT:NextJS 做的,提供前端和后端的服务

mongo:聊天记录、历史对话等数据

pg:存向量

MySQL:_

M3E:向量化模型

运行 chatglm3-6b

https://github.com/THUDM/ChatGLM3

克隆项目后,我们安装依赖,记得装 CUDA 驱动。

我们需要的就是 openai_api_demo 里面的https://github.com/THUDM/ChatGLM3/blob/main/openai_api_demo/api_server.py

这里需要注意的是,你可以选择提前下载好模型,或者是运行的时候再下载,提前下载可能会快很多~

glm3-6b,这个比较好运行,网上教程非常多,只要跑起来就可以,这里不赘述了

ed8272ae3599f0d00c32bc9b6f989a08

注意是 GLM3 的不要 下载错了

api_server.py

这里给出来,修改了一下前面的路径,因为我已经提前下载好了模型

import os
import time
import tiktoken
import torch
import uvicorn

from fastapi import FastAPI, HTTPException, Response
from fastapi.middleware.cors import CORSMiddleware

from contextlib import asynccontextmanager
from typing import List, Literal, Optional, Union
from loguru import logger
from pydantic import BaseModel, Field
from transformers import AutoTokenizer, AutoModel
from utils import process_response, generate_chatglm3, generate_stream_chatglm3
from sentence_transformers import SentenceTransformer

from sse_starlette.sse import EventSourceResponse


print(torch.__version__)
print(torch.cuda.is_available())

# Set up limit request time
EventSourceResponse.DEFAULT_PING_INTERVAL = 1000

# 获取当前脚本的目录
current_dir = os.path.dirname(os.path.abspath(__file__))

# 构建相对于当前脚本的模型目录路径
model_dir = os.path.abspath(os.path.join(current_dir, "..", "..", "chatglm3-6b"))

print(f"model_dir: {model_dir}")

# 设置环境变量
os.environ['MODEL_PATH'] = model_dir
os.environ['TOKENIZER_PATH'] = model_dir

print("MODEL_PATH:", os.getenv('MODEL_PATH'))
print("TOKENIZER_PATH:", os.getenv('TOKENIZER_PATH'))

# set LLM path
MODEL_PATH = os.environ.get('MODEL_PATH', 'THUDM/chatglm3-6b')
TOKENIZER_PATH = os.environ.get("TOKENIZER_PATH", MODEL_PATH)

# set Embedding Model path
EMBEDDING_PATH = os.environ.get('EMBEDDING_PATH', 'BAAI/bge-m3')


@asynccontextmanager
async def lifespan(app: FastAPI):
yield
if torch.cuda.is_available():
torch.cuda.empty_cache()
torch.cuda.ipc_collect()


app = FastAPI(lifespan=lifespan)

app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)


class ModelCard(BaseModel):
id: str
object: str = "model"
created: int = Field(default_factory=lambda: int(time.time()))
owned_by: str = "owner"
root: Optional[str] = None
parent: Optional[str] = None
permission: Optional[list] = None


class ModelList(BaseModel):
object: str = "list"
data: List[ModelCard] = []


class FunctionCallResponse(BaseModel):
name: Optional[str] = None
arguments: Optional[str] = None


class ChatMessage(BaseModel):
role: Literal["user", "assistant", "system", "function"]
content: str = None
name: Optional[str] = None
function_call: Optional[FunctionCallResponse] = None


class DeltaMessage(BaseModel):
role: Optional[Literal["user", "assistant", "system"]] = None
content: Optional[str] = None
function_call: Optional[FunctionCallResponse] = None


## for Embedding
class EmbeddingRequest(BaseModel):
input: Union[List[str], str]
model: str


class CompletionUsage(BaseModel):
prompt_tokens: int
completion_tokens: int
total_tokens: int


class EmbeddingResponse(BaseModel):
data: list
model: str
object: str
usage: CompletionUsage


# for ChatCompletionRequest

class UsageInfo(BaseModel):
prompt_tokens: int = 0
total_tokens: int = 0
completion_tokens: Optional[int] = 0


class ChatCompletionRequest(BaseModel):
model: str
messages: List[ChatMessage]
temperature: Optional[float] = 0.8
top_p: Optional[float] = 0.8
max_tokens: Optional[int] = None
stream: Optional[bool] = False
tools: Optional[Union[dict, List[dict]]] = None
repetition_penalty: Optional[float] = 1.1


class ChatCompletionResponseChoice(BaseModel):
index: int
message: ChatMessage
finish_reason: Literal["stop", "length", "function_call"]


class ChatCompletionResponseStreamChoice(BaseModel):
delta: DeltaMessage
finish_reason: Optional[Literal["stop", "length", "function_call"]]
index: int


class ChatCompletionResponse(BaseModel):
model: str
id: str
object: Literal["chat.completion", "chat.completion.chunk"]
choices: List[Union[ChatCompletionResponseChoice, ChatCompletionResponseStreamChoice]]
created: Optional[int] = Field(default_factory=lambda: int(time.time()))
usage: Optional[UsageInfo] = None


@app.get("/health")
async def health() -> Response:
"""Health check."""
return Response(status_code=200)


@app.post("/v1/embeddings", response_model=EmbeddingResponse)
async def get_embeddings(request: EmbeddingRequest):
if isinstance(request.input, str):
embeddings = [embedding_model.encode(request.input)]
else:
embeddings = [embedding_model.encode(text) for text in request.input]
embeddings = [embedding.tolist() for embedding in embeddings]

def num_tokens_from_string(string: str) -> int:
"""
Returns the number of tokens in a text string.
use cl100k_base tokenizer
"""
encoding = tiktoken.get_encoding('cl100k_base')
num_tokens = len(encoding.encode(string))
return num_tokens

response = {
"data": [
{
"object": "embedding",
"embedding": embedding,
"index": index
}
for index, embedding in enumerate(embeddings)
],
"model": request.model,
"object": "list",
"usage": CompletionUsage(
prompt_tokens=sum(len(text.split()) for text in request.input),
completion_tokens=0,
total_tokens=sum(num_tokens_from_string(text) for text in request.input),
)
}
return response


@app.get("/v1/models", response_model=ModelList)
async def list_models():
model_card = ModelCard(
id="chatglm3-6b"
)
return ModelList(
data=[model_card]
)


@app.post("/v1/chat/completions", response_model=ChatCompletionResponse)
async def create_chat_completion(request: ChatCompletionRequest):
global model, tokenizer

if len(request.messages) < 1 or request.messages[-1].role == "assistant":
raise HTTPException(status_code=400, detail="Invalid request")

gen_params = dict(
messages=request.messages,
temperature=request.temperature,
top_p=request.top_p,
max_tokens=request.max_tokens or 1024,
echo=False,
stream=request.stream,
repetition_penalty=request.repetition_penalty,
tools=request.tools,
)
logger.debug(f"==== request ====\n{gen_params}")

if request.stream:

# Use the stream mode to read the first few characters, if it is not a function call, direct stram output
predict_stream_generator = predict_stream(request.model, gen_params)
output = next(predict_stream_generator)
if not contains_custom_function(output):
return EventSourceResponse(predict_stream_generator, media_type="text/event-stream")

# Obtain the result directly at one time and determine whether tools needs to be called.
logger.debug(f"First result output:\n{output}")

function_call = None
if output and request.tools:
try:
function_call = process_response(output, use_tool=True)
except:
logger.warning("Failed to parse tool call")

# CallFunction
if isinstance(function_call, dict):
function_call = FunctionCallResponse(**function_call)

"""
In this demo, we did not register any tools.
You can use the tools that have been implemented in our `tools_using_demo` and implement your own streaming tool implementation here.
Similar to the following method:
function_args = json.loads(function_call.arguments)
tool_response = dispatch_tool(tool_name: str, tool_params: dict)
"""
tool_response = ""

if not gen_params.get("messages"):
gen_params["messages"] = []

gen_params["messages"].append(ChatMessage(
role="assistant",
content=output,
))
gen_params["messages"].append(ChatMessage(
role="function",
name=function_call.name,
content=tool_response,
))

# Streaming output of results after function calls
generate = predict(request.model, gen_params)
return EventSourceResponse(generate, media_type="text/event-stream")

else:
# Handled to avoid exceptions in the above parsing function process.
generate = parse_output_text(request.model, output)
return EventSourceResponse(generate, media_type="text/event-stream")

# Here is the handling of stream = False
response = generate_chatglm3(model, tokenizer, gen_params)

# Remove the first newline character
if response["text"].startswith("\n"):
response["text"] = response["text"][1:]
response["text"] = response["text"].strip()

usage = UsageInfo()
function_call, finish_reason = None, "stop"
if request.tools:
try:
function_call = process_response(response["text"], use_tool=True)
except:
logger.warning("Failed to parse tool call, maybe the response is not a tool call or have been answered.")

if isinstance(function_call, dict):
finish_reason = "function_call"
function_call = FunctionCallResponse(**function_call)

message = ChatMessage(
role="assistant",
content=response["text"],
function_call=function_call if isinstance(function_call, FunctionCallResponse) else None,
)

logger.debug(f"==== message ====\n{message}")

choice_data = ChatCompletionResponseChoice(
index=0,
message=message,
finish_reason=finish_reason,
)
task_usage = UsageInfo.model_validate(response["usage"])
for usage_key, usage_value in task_usage.model_dump().items():
setattr(usage, usage_key, getattr(usage, usage_key) + usage_value)

return ChatCompletionResponse(
model=request.model,
id="", # for open_source model, id is empty
choices=[choice_data],
object="chat.completion",
usage=usage
)


async def predict(model_id: str, params: dict):
global model, tokenizer

choice_data = ChatCompletionResponseStreamChoice(
index=0,
delta=DeltaMessage(role="assistant"),
finish_reason=None
)
chunk = ChatCompletionResponse(model=model_id, id="", choices=[choice_data], object="chat.completion.chunk")
yield "{}".format(chunk.model_dump_json(exclude_unset=True))

previous_text = ""
for new_response in generate_stream_chatglm3(model, tokenizer, params):
decoded_unicode = new_response["text"]
delta_text = decoded_unicode[len(previous_text):]
previous_text = decoded_unicode

finish_reason = new_response["finish_reason"]
if len(delta_text) == 0 and finish_reason != "function_call":
continue

function_call = None
if finish_reason == "function_call":
try:
function_call = process_response(decoded_unicode, use_tool=True)
except:
logger.warning(
"Failed to parse tool call, maybe the response is not a tool call or have been answered.")

if isinstance(function_call, dict):
function_call = FunctionCallResponse(**function_call)

delta = DeltaMessage(
content=delta_text,
role="assistant",
function_call=function_call if isinstance(function_call, FunctionCallResponse) else None,
)

choice_data = ChatCompletionResponseStreamChoice(
index=0,
delta=delta,
finish_reason=finish_reason
)
chunk = ChatCompletionResponse(
model=model_id,
id="",
choices=[choice_data],
object="chat.completion.chunk"
)
yield "{}".format(chunk.model_dump_json(exclude_unset=True))

choice_data = ChatCompletionResponseStreamChoice(
index=0,
delta=DeltaMessage(),
finish_reason="stop"
)
chunk = ChatCompletionResponse(
model=model_id,
id="",
choices=[choice_data],
object="chat.completion.chunk"
)
yield "{}".format(chunk.model_dump_json(exclude_unset=True))
yield '[DONE]'


def predict_stream(model_id, gen_params):
"""
The function call is compatible with stream mode output.
The first seven characters are determined.
If not a function call, the stream output is directly generated.
Otherwise, the complete character content of the function call is returned.
:param model_id:
:param gen_params:
:return:
"""
output = ""
is_function_call = False
has_send_first_chunk = False
for new_response in generate_stream_chatglm3(model, tokenizer, gen_params):
decoded_unicode = new_response["text"]
delta_text = decoded_unicode[len(output):]
output = decoded_unicode

# When it is not a function call and the character length is> 7,
# try to judge whether it is a function call according to the special function prefix
if not is_function_call and len(output) > 7:

# Determine whether a function is called
is_function_call = contains_custom_function(output)
if is_function_call:
continue

# Non-function call, direct stream output
finish_reason = new_response["finish_reason"]

# Send an empty string first to avoid truncation by subsequent next() operations.
if not has_send_first_chunk:
message = DeltaMessage(
content="",
role="assistant",
function_call=None,
)
choice_data = ChatCompletionResponseStreamChoice(
index=0,
delta=message,
finish_reason=finish_reason
)
chunk = ChatCompletionResponse(
model=model_id,
id="",
choices=[choice_data],
created=int(time.time()),
object="chat.completion.chunk"
)
yield "{}".format(chunk.model_dump_json(exclude_unset=True))

send_msg = delta_text if has_send_first_chunk else output
has_send_first_chunk = True
message = DeltaMessage(
content=send_msg,
role="assistant",
function_call=None,
)
choice_data = ChatCompletionResponseStreamChoice(
index=0,
delta=message,
finish_reason=finish_reason
)
chunk = ChatCompletionResponse(
model=model_id,
id="",
choices=[choice_data],
created=int(time.time()),
object="chat.completion.chunk"
)
yield "{}".format(chunk.model_dump_json(exclude_unset=True))

if is_function_call:
yield output
else:
yield '[DONE]'


async def parse_output_text(model_id: str, value: str):
"""
Directly output the text content of value
:param model_id:
:param value:
:return:
"""
choice_data = ChatCompletionResponseStreamChoice(
index=0,
delta=DeltaMessage(role="assistant", content=value),
finish_reason=None
)
chunk = ChatCompletionResponse(model=model_id, id="", choices=[choice_data], object="chat.completion.chunk")
yield "{}".format(chunk.model_dump_json(exclude_unset=True))

choice_data = ChatCompletionResponseStreamChoice(
index=0,
delta=DeltaMessage(),
finish_reason="stop"
)
chunk = ChatCompletionResponse(model=model_id, id="", choices=[choice_data], object="chat.completion.chunk")
yield "{}".format(chunk.model_dump_json(exclude_unset=True))
yield '[DONE]'


def contains_custom_function(value: str) -> bool:
"""
Determine whether 'function_call' according to a special function prefix.
For example, the functions defined in "tools_using_demo/tool_register.py" are all "get_xxx" and start with "get_"
[Note] This is not a rigorous judgment method, only for reference.
:param value:
:return:
"""
return value and 'get_' in value


if __name__ == "__main__":
# Load LLM
tokenizer = AutoTokenizer.from_pretrained(os.getenv('TOKENIZER_PATH'), trust_remote_code=True)
model = AutoModel.from_pretrained(os.getenv('MODEL_PATH'), trust_remote_code=True, device_map="auto").eval()

# load Embedding
embedding_model = SentenceTransformer(EMBEDDING_PATH, device="cuda")
uvicorn.run(app, host='0.0.0.0', port=8888, workers=1)

运行成功后

85e97e5d6213b8e74cd7faa2622a355b### 安装 M3E

可以参考官方给出的镜像,这个自己搞容易出问题,直接使用镜像最方便,https://doc.fastai.site/docs/development/custom-models/m3e/

配置 OneAPI

重点来了,OneAPI 目前这个地方有个坑,你参考这篇文章的时候,https://doc.fastai.site/docs/development/custom-models/m3e/#接入-one-api,记得 Base URL 一定要填写具体的 IP 地址。

我的 M3E 在 WSL 里面

2090aa22ad0e2333c3349df4bcff350e### 我的 ChatGLM API 在本机本地运行
c04de4dba58351b4174778516b263eb1### 测试

如果响应时间特别长,就是有问题

ed0d8bf11e9e4b189ad5dbcde10b5bb7### 增加 config.json 配置

这里我直接给出我增加的内容,配置文件说明请看 https://doc.fastai.site/docs/development/configuration/

{
"feConfigs": {
"lafEnv": "https://laf.dev"
},
"systemEnv": {
"openapiPrefix": "fastgpt",
"vectorMaxProcess": 15,
"qaMaxProcess": 15,
"pgHNSWEfSearch": 100
},
"llmModels": [
{
"model": "gpt-3.5-turbo",
"name": "gpt-3.5-turbo",
"maxContext": 16000,
"avatar": "/imgs/model/openai.svg",
"maxResponse": 4000,
"quoteMaxToken": 13000,
"maxTemperature": 1.2,
"charsPointsPrice": 0,
"censor": false,
"vision": false,
"datasetProcess": true,
"usedInClassify": true,
"usedInExtractFields": true,
"usedInToolCall": true,
"usedInQueryExtension": true,
"toolChoice": true,
"functionCall": true,
"customCQPrompt": "",
"customExtractPrompt": "",
"defaultSystemChatPrompt": "",
"defaultConfig": {}
},
{
"model": "gpt-4-0125-preview",
"name": "gpt-4-turbo",
"avatar": "/imgs/model/openai.svg",
"maxContext": 125000,
"maxResponse": 4000,
"quoteMaxToken": 100000,
"maxTemperature": 1.2,
"charsPointsPrice": 0,
"censor": false,
"vision": false,
"datasetProcess": false,
"usedInClassify": true,
"usedInExtractFields": true,
"usedInToolCall": true,
"usedInQueryExtension": true,
"toolChoice": true,
"functionCall": false,
"customCQPrompt": "",
"customExtractPrompt": "",
"defaultSystemChatPrompt": "",
"defaultConfig": {}
},
{
"model": "gpt-4-vision-preview",
"name": "gpt-4-vision",
"avatar": "/imgs/model/openai.svg",
"maxContext": 128000,
"maxResponse": 4000,
"quoteMaxToken": 100000,
"maxTemperature": 1.2,
"charsPointsPrice": 0,
"censor": false,
"vision": true,
"datasetProcess": false,
"usedInClassify": false,
"usedInExtractFields": false,
"usedInToolCall": false,
"usedInQueryExtension": false,
"toolChoice": true,
"functionCall": false,
"customCQPrompt": "",
"customExtractPrompt": "",
"defaultSystemChatPrompt": "",
"defaultConfig": {}
},
{
"model": "chatglm3-6b",
"name": "chatglm3-6b",
"maxContext": 8000,
"maxResponse": 8000,
"quoteMaxToken": 2000,
"maxTemperature": 1,
"vision": false,
"defaultSystemChatPrompt": ""
}
],
"vectorModels": [
{
"model": "m3e",
"name": "M3E(测试使用)",
"price": 0.1,
"defaultToken": 500,
"maxToken": 1800
}
],
"reRankModels": [],
"audioSpeechModels": [
{
"model": "tts-1",
"name": "OpenAI TTS1",
"charsPointsPrice": 0,
"voices": [
{
"label": "Alloy",
"value": "alloy",
"bufferId": "openai-Alloy"
},
{
"label": "Echo",
"value": "echo",
"bufferId": "openai-Echo"
},
{
"label": "Fable",
"value": "fable",
"bufferId": "openai-Fable"
},
{
"label": "Onyx",
"value": "onyx",
"bufferId": "openai-Onyx"
},
{
"label": "Nova",
"value": "nova",
"bufferId": "openai-Nova"
},
{
"label": "Shimmer",
"value": "shimmer",
"bufferId": "openai-Shimmer"
}
]
}
],
"whisperModel": {
"model": "whisper-1",
"name": "Whisper1",
"charsPointsPrice": 0
}
}

常见问题

  • 使用WSL最好电脑内存大点,WSL这玩意很吃内存
  • 能用docker就用docker,很多都是因为配置的问题出错
  • 显存不够可以量化,不行就用API

参考链接

https://doc.fastai.site/docs/development/docker/

https://github.com/THUDM/ChatGLM3/tree/main/openai_api_demo

https://huggingface.co/THUDM/chatglm3-6b

https://www.modelscope.cn/models/xrunda/m3e-base/summary

https://github.com/labring/FastGPT

https://github.com/labring/sealos

https://github.com/songquanpeng/one-api

如果您喜欢这篇文章,不妨给它点个赞并收藏,感谢您的支持!

单元测试和集成测试的区别

· 阅读需 3 分钟
素明诚
Full stack development

单元测试(Unit Testing)

单元测试是对软件中的最小可测试部分,通常是单个函数或方法进行测试,以确保它们能够正确执行预定的任务。可以说单元测试是针对每一个函数或方法都是一块砖,单元测试就是确保每块砖本身没有缺陷。

为什么要进行单元测试

确保质量 通过单元测试,开发者可以确认每个基本部件都按照预期工作。

快速识别问题 如果出现问题,很容易定位到具体的函数或方法,因为测试是独立进行的。

单元测试的特点

独立性 每个测试独立于其他测试,确保测试结果的准确性。

无外部依赖 避免使用数据库或网络服务等外部系统,简单点说就是,只是针对特定的组件进行测试,不连接缓存或者是数据库,也不会请求其他的模块~

集成测试(Integration Testing)

集成测试是在单元测试之后进行的,目的是检查多个单元(比如几个函数或模块)组合在一起时是否能够协同工作。如果说单元测试是检查每块砖,那么集成测试就是检查砖与砖之间的粘合是否牢固。

为什么要进行集成测试

接口和数据流 确保不同部分之间的接口正确无误,数据能够正确传递。

发现协同问题 单元测试可能漏掉的问题,在集成测试中往往可以被发现,一般来说只要你对其他业务接口的数据理解没问题,集成测试也基本没问题

集成测试的特点

逐步集成 从测试最小的组合开始,逐步增加直到整个系统被测试完毕。很多人理解集成测试就是一下子都笼起来,全部测一下。其实也是可以一部分一部分测的

模拟真实环境 尽可能在接近生产环境的设置中进行测试,以模拟真实操作。

总结

单元测试就是为了测试某个具体的功能是否能通过,有 bug 就改完再测,不行就再来亿遍。集成测试你的代码基本跑通,接下来要跟第三方进行集成测试。

Python 根据 Markdown 目录创建文件文件夹

· 阅读需 1 分钟
素明诚
Full stack development

将脚本保存为 create_dirs_from_md.py

将您的目录结构粘贴到 md_structure 变量中。

在终端运行脚本:python3 create_dirs_from_md.py

17adf28490d266622875f6dd7c4cbabe

创建成功

import os
import re

md_structure = '''
- cmd/
- main.go
- internal/
- domain/
- model/
- order.go
- repository/
- order_repository.go
- application/
- service/
- order_service.go
- infrastructure/
- repository/
- order_repository_impl.go
- db/
- database.go
- interfaces/
- handler/
- order_handler.go
- di/
- wire.go
- wire_gen.go
'''

def parse_md_structure(md_text):
lines = md_text.strip().split('\n')
stack = []
paths = []
for line in lines:
# 计算缩进级别
indent = len(line) - len(line.lstrip(' '))
level = indent // 2 # 每两个空格一个级别

# 提取名称,去除前面的符号和空格
name = line.strip().lstrip('-').strip()

# 更新堆栈
while len(stack) > level:
stack.pop()
stack.append(name)

# 构建路径
path = os.path.join(*stack)
paths.append(path)

return paths

def create_dirs_and_files(paths):
for path in paths:
if path.endswith('/'):
# 是目录
dir_path = path.rstrip('/')
os.makedirs(dir_path, exist_ok=True)
print(f"Created directory: {dir_path}")
elif '.' in os.path.basename(path):
# 是文件
dir_name = os.path.dirname(path)
if dir_name and not os.path.exists(dir_name):
os.makedirs(dir_name, exist_ok=True)
print(f"Created directory: {dir_name}")
with open(path, 'w') as f:
pass
print(f"Created file: {path}")
else:
# 是目录(没有斜杠,但也不是文件)
os.makedirs(path, exist_ok=True)
print(f"Created directory: {path}")

if __name__ == "__main__":
paths = parse_md_structure(md_structure)
create_dirs_and_files(paths)

Linux 系统中 所有权Ownership和操作权Permissions

· 阅读需 3 分钟
素明诚
Full stack development

所有权(Ownership)

文件和目录的所有权包括两个主要属性:用户所有者(User Owner)和组所有者(Group Owner)。

用户所有者:指对文件或目录具有特定权限的用户。通常,用户所有者可以修改文件内容、改变文件权限等。

组所有者:指被分配给文件或目录的用户组。组内的所有成员可以根据组权限对文件或目录进行操作。

更改所有权通常通过 chown 命令完成,这对于系统安全管理、文件共享和用户数据隔离至关重要。

权限(Permissions)

权限决定了用户对文件或目录可以执行的操作,分为所有者权限、组权限和其他用户(Others)权限

所有者权限:定义文件或目录的所有者可以执行的操作。

组权限:定义同一用户组内的用户可以执行的操作。

其他用户权限:定义不属于文件所属组的其他用户可以执行的操作。

权限设置通常通过 chmod 命令进行控制,包括读取(r)、写入(w)和执行(x)权限。这些权限对于保护敏感数据、维护系统安全和控制程序执行至关重要。

为什么需要区分所有权和权限?

从设计角度来看,区分所有权和权限是为了实现灵活且精细的访问控制

责任分离:所有权确定了谁对文件或目录负责,而权限则定义了不同用户可以对其执行的操作。这种分离使系统能够明确归属,同时独立地管理访问控制。

精细的访问控制:通过区分所有权和权限,系统可以为所有者、所属组和其他用户分别设置不同的权限。这种设计允许文件所有者限制自己的权限,或者授予其他用户特定的访问权,实现更精细的控制。

灵活性:这种区分使得权限设置更加灵活,适应各种使用场景。例如,某个文件可以由用户 A 拥有,但权限设置允许用户 B 读取或修改,而无需更改所有权。

安全性增强:系统管理员可以独立于所有权来调整权限,以强化安全策略。例如,即使用户是某个文件的所有者,管理员也可以限制其权限,防止潜在的误用或恶意操作。

资源管理:在多用户环境中,区分所有权和权限有助于更有效地管理系统资源。用户可以共享文件而不失去对其的控制权,促进协作的同时保障数据安全。

历史设计选择:Unix 系统最初设计时,就采用了这种所有权和权限分离的模型。它提供了一个简单 yet 强大的机制,避免了复杂的访问控制列表(ACL),满足了当时对效率和性能的需求。

Docker DanglingUnused Images

· 阅读需 2 分钟
素明诚
Full stack development

Dangling Images(悬空镜像)

定义这些镜像在本地存在,但是在 Docker 仓库中已经不存在对应的标签了。通常是因为镜像的标签被更新或删除导致的。

特点它们没有标签,只有镜像 ID。

产生原因

  • 构建一个新版本的镜像,自动取消旧版本镜像的标签。
  • 手动删除一个镜像的标签。
  • 使用docker pull拉取一个已有标签的新版本镜像,旧版本镜像标签被取消。

识别方法使用命令docker images -f dangling=true列出所有悬空镜像。

Unused Images(未使用镜像)

定义这些镜像不是任何容器的基础镜像,也不是任何镜像的父镜像。

特点它们可能有标签,但是已经不被需要了。

产生原因

  • 某个容器或者镜像被删除,依赖于它的镜像层也随之变为未使用。
  • 手动删除一个容器或镜像。

识别方法使用命令docker images -f dangling=false并手动识别那些未被使用的镜像。Docker 目前没有提供自动识别 Unused Images 的命令。

清理 Dangling Images

使用命令docker rmi $(docker images -f dangling=true -q)删除所有悬空镜像。

使用 Docker 1.13 版本提供的docker system prune命令自动清理悬空镜像。

清理 Unused Images

手动识别并删除那些已知不再需要的镜像,使用docker rmi <image_id>命令。

谨慎使用docker system prune -a命令自动删除所有未使用镜像,此操作不可恢复。

清理时的注意事项

清理前确保不需要这些镜像,清理操作不可恢复。

如果一个悬空镜像同时也是一个未使用镜像,使用docker image prune或者docker system prune命令就足够了。

定期清理不需要的镜像,以节省磁盘空间。但不要过于频繁。

KubernetesK8s集群 Docker 作为容器运行时

· 阅读需 5 分钟
素明诚
Full stack development

准备代理

export HTTPS_PROXY="http://172.22.220.64:7890"

版本问题

三台 Ubuntu 22.04.4 LTS x86_64 机器上搭建 Kubernetes 集群。使用的 Docker 版本至少是 19.03,因为这个版本支持 Kubernetes 所需的特性和配置。确保 Docker 的 cgroup 驱动设置为systemd,以与 kubelet 保持一致,避免资源管理上的潜在冲突。

环境准备

设置主机名

每台机器上设置适当的主机名并更新 /etc/hosts 文件以便节点间能够相互解析。

在每台机器上执行以下命令

# 在 k8s-master 上
sudo hostnamectl set-hostname k8s-master
echo "172.22.220.64 k8s-master" | sudo tee -a /etc/hosts

# 在 k8s-worker1 上
sudo hostnamectl set-hostname k8s-worker1
echo "192.168.33.110 k8s-worker1" | sudo tee -a /etc/hosts

# 在 k8s-worker2 上
sudo hostnamectl set-hostname k8s-worker2
echo "192.168.33.111 k8s-worker2" | sudo tee -a /etc/hosts

禁用 Swap

Kubernetes 要求禁用 swap。在每台机器上执行

sudo swapoff -a
# 永久禁用,注释掉 /etc/fstab 中相关的 swap 行
sudo sed -i '/ swap / s/^/#/' /etc/fstab

加载必要的内核模块

在每台机器上执行

sudo modprobe overlay
sudo modprobe br_netfilter

设置系统参数

在每台机器上添加 Kubernetes 推荐的 sysctl 参数

cat <<EOF | sudo tee /etc/sysctl.d/k8s.conf
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-iptables = 1
net.ipv4.ip_forward = 1
EOF
sudo sysctl --system

安装 Docker、kubeadm、kubelet 和 kubectl

安装 Docker

如果已经安装了 Docker,确认一下 Docker 是否正在运行

sudo systemctl status docker

没有安装,先安装一下

apt install docker.io

添加 Kubernetes 仓库

在每台机器上执行

sudo apt-get update
sudo apt-get install -y apt-transport-https ca-certificates curl
curl -s https://packages.cloud.google.com/apt/doc/apt-key.gpg | sudo apt-key add -
echo "deb https://apt.kubernetes.io/ kubernetes-xenial main" | sudo tee /etc/apt/sources.list.d/kubernetes.list

安装 kubeadm, kubelet 和 kubectl

kubelet运行在所有集群节点上的代理,负责启动和管理容器应用。

kubeadm工具,用于快速设置和管理 Kubernetes 集群的基础架构。

kubectl命令行工具,用于与 Kubernetes 集群交互和管理资源。

sudo apt-get update
sudo apt-get install -y kubelet-1.23.6 kubeadm-1.23.6 kubectl-1.23.6
sudo apt-mark hold kubelet-1.23.6 kubeadm-1.23.6 kubectl-1.23.6

# 如果你已经安装,可以先删除再安装指定版本
sudo apt-get remove -y kubelet kubeadm kubectl

hold 是设置保留状态,不会自动更新

配置 docker

nano /etc/docker/daemon.json
{
"experimental": true,
"features": {
"buildkit": true
},
"exec-opts": ["native.cgroupdriver=systemd"],
"log-driver": "json-file",
"log-opts": {
"max-size": "100m"
},
"storage-driver": "overlay2"
}

重启

sudo systemctl restart docker

配置 kubelet

nano /etc/systemd/system/kubelet.service.d/10-kubeadm.conf
# Note This dropin only works with kubeadm and kubelet v1.11+
[Service]
Environment="KUBELET_KUBECONFIG_ARGS=--bootstrap-kubeconfig=/etc/kubernetes/bootstrap-kubelet.conf --kubeconfig=/etc/kubernetes/kubelet.conf"
Environment="KUBELET_CONFIG_ARGS=--config=/var/lib/kubelet/config.yaml"
# This is a file that "kubeadm init" and "kubeadm join" generates at runtime, populating the KUBELET_KUBEADM_ARGS variable dynamically
EnvironmentFile=-/var/lib/kubelet/kubeadm-flags.env
# This is a file that the user can use for overrides of the kubelet args as a last resort. Preferably, the user should use
# the .NodeRegistration.KubeletExtraArgs object in the configuration files instead. KUBELET_EXTRA_ARGS should be sourced from this file.
EnvironmentFile=-/etc/default/kubelet
ExecStart=
ExecStart=/usr/bin/kubelet $KUBELET_KUBECONFIG_ARGS $KUBELET_CONFIG_ARGS $KUBELET_KUBEADM_ARGS $KUBELET_EXTRA_ARGS
Environment="KUBELET_CGROUP_ARGS=--cgroup-driver=systemd"
nano /var/lib/kubelet/config.yaml
sudo chown rootroot /var/lib/kubelet/config.yaml
sudo chmod 644 /var/lib/kubelet/config.yaml

重启

sudo systemctl daemon-reload
sudo systemctl restart kubelet

初始化 Kubernetes 主节点

查看要下载的内容

kubeadm config images list

使用阿里云 Kubernetes 镜像的镜像加速服务

**10.244.0.0/16**这个是 Flannel 默认的 Pod 网络地址范围。如果你使用 Flannel 作为网络插件,通常使用这个 CIDR。

**192.168.0.0/16**这是 Calico 的默认地址范围。如果你使用 Calico,通常会指定这个范围,不过你也可以自定义。

kubeadm init --pod-network-cidr=192.168.0.0/16 --image-repository registry.cn-hangzhou.aliyuncs.com/google_containers --kubernetes-version=v1.23.0

不使用阿里云,直接在 k8s-master 上执行,初始化,这里的 -E 选项告诉 sudo 保留用户的环境变量,这样 kubeadm 就可以使用前面设置的代理环境变量。

sudo -E kubeadm init --pod-network-cidr=10.244.0.0/16

记录下输出中的 kubeadm join 命令,这是后面工作节点需要使用的命令。

Your Kubernetes control-plane has initialized successfully!

To start using your cluster, you need to run the following as a regular user

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u)$(id -g) $HOME/.kube/config

Alternatively, if you are the root user, you can run

export KUBECONFIG=/etc/kubernetes/admin.conf

You should now deploy a pod network to the cluster.
Run "kubectl apply -f [podnetwork].yaml" with one of the options listed at
https//kubernetes.io/docs/concepts/cluster-administration/addons/

Then you can join any number of worker nodes by running the following on each as root

kubeadm init phase certs apiserver --apiserver-cert-extra-sans=172.22.220.64,kubernetes.default,kubernetes.default.svc,kubernetes.default.svc.cluster,kubernetes.default.svc.cluster.local

配置 kubectl

在 k8s-master 上,配置用户的 kubeconfig 文件以使用 kubectl

mkdir -p $HOME/.kube
sudo cp -i /etc/kubernetes/admin.conf $HOME/.kube/config
sudo chown $(id -u)$(id -g) $HOME/.kube/config

如果证书有问题,重新生成证书

手动删除现有的 apiserver 证书文件

rm /etc/kubernetes/pki/apiserver.*

再次运行证书生成命令

kubeadm init phase certs apiserver --apiserver-cert-extra-sans=172.22.220.64,kubernetes.default,kubernetes.default.svc,kubernetes.default.svc.cluster,kubernetes.default.svc.cluster.local

再次重启 apiserver 容器

docker ps | grep kube-apiserver | grep -v pause
docker kill <container-id>

再次检查新生成的证书

openssl x509 -in /etc/kubernetes/pki/apiserver.crt -text -noout

如果新证书的内容(尤其是序列号)与旧证书不同,就说明证书已经更新成功。

安装 Pod 网络插件

可以选择一个网络插件,例如 Calico,并在 k8s-master 上执行

kubectl apply -f https//docs.projectcalico.org/manifests/calico.yaml
kubectl get nodes

加入工作节点

在 k8s-worker1 和 k8s-worker2 上,使用之前记录的 kubeadm join 命令来加入集群

kubeadm join 172.22.220.646443 --token 6bqr0n.5dlu623stgokbvdd \
--discovery-token-ca-cert-hash sha256851c48c250ee78aa569a77c935f14ee6edcebc6b004655e52ac775bcc89cf013

验证集群状态

在 k8s-master 上,检查节点和 Pod 的状态

kubectl get nodes
kubectl get pods --all-namespaces

监控节点变化

kubectl get nodes -w

安装并配置 Helm

在你的本地机器或管理机上安装 Helm。这通常涉及到下载 Helm 的二进制文件并将其配置到适当的路径。你还需要初始化 Helm 的服务端组件 Tiller(注意从 Helm v3 开始,已经移除了 Tiller)。

对于 Helm 3,直接初始化即可

helm repo add stable https//charts.helm.sh/stable
helm repo update

验证 Helm 是否正确安装并配置,可以尝试安装一个简单的 Helm chart 来测试

helm install stable/mysql --generate-name

如果这个命令成功执行,并且能够看到 Pod 正在运行状态,则说明 Helm 已准备好用于部署应用。

卸载 K8S 所有组件

kubeadm reset -f
systemctl stop kubelet
systemctl stop docker
rm -rf /var/lib/cni/
rm -rf /var/lib/kubelet/*
rm -rf /etc/cni/
rm -rf /etc/kubernetes/
rm -rf /var/lib/docker/
apt-get remove docker docker-engine docker.io containerd runc

Linux 配置系统代理临时代理

· 阅读需 2 分钟
素明诚
Full stack development

查看代理

echo $http_proxy
echo $https_proxy
echo $ftp_proxy
echo $no_proxy

临时配置代理

在当前终端会话中设置代理,这种设置只会在当前终端窗口有效,关闭窗口后设置会失效

export HTTP_PROXY="http://172.22.220.64:7890"
export HTTPS_PROXY="http://172.22.220.64:7890"

这样设置后,仅当前终端会话中的命令会使用这些代理设置。如果你想在当前会话中取消代理,可以使用 unset 命令

unset HTTP_PROXY
unset HTTPS_PROXY

持久配置代理

要让代理设置在所有会话和重启后依然有效,你可以将这些环境变量添加到你的用户配置文件中,例如 .bashrc.bash_profile 文件中

打开你的用户主目录下的 .bashrc.bash_profile 文件

nano ~/.bashrc

在文件的末尾添加以下内容

export HTTP_PROXY="http://172.22.220.64:7890"
export HTTPS_PROXY="http://172.22.220.64:7890"

保存并关闭文件。使更改生效

source ~/.bashrc

这样,每次用户登录时,系统都会自动加载这些设置,从而为所有终端会话启用代理。

配置不使用代理的地址

在设置了全局代理的情况下,可能需要某些地址不通过代理访问,比如本地地址或特定的内部网站。可以使用 no_proxy 环境变量来实现

export NO_PROXY="localhost,127.0.0.1,.localdomain.com"

添加到 .bashrc.bash_profile 中,同时加入上述的代理设置中

export HTTP_PROXY="http://172.22.220.64:7890"
export HTTPS_PROXY="http://172.22.220.64:7890"
export NO_PROXY="localhost,127.0.0.1,.localdomain.com"

配置系统级别的环境变量

在 Linux 系统中,你可以通过修改 /etc/environment 文件来设置系统级别的代理环境变量,这样不需要对每个用户的 .bashrc 进行单独配置。

编辑 /etc/environment 文件

使用你喜欢的文本编辑器以超级用户权限打开 /etc/environment 文件。

sudo nano /etc/environment

添加代理设置

/etc/environment 文件中添加你的代理配置。这个文件中的每个条目都应该是简单的 KEY="value" 格式。添加 HTTP 和 HTTPS 代理配置如下

HTTP_PROXY="http://172.22.220.64:7890"
HTTPS_PROXY="http://172.22.220.64:7890"
NO_PROXY="localhost,127.0.0.1,.localdomain.com"

保存并关闭文件

保存更改并关闭编辑器。如果你使用的是 nano,可以按 Ctrl+X 然后按 Y 确认保存,最后按 Enter 保存文件。

使更改生效

更改 /etc/environment 后,需要注销再登录或重启系统,以使设置对所有用户生效。

Ubuntu 上安装 Docker 和 Docker Compose

· 阅读需 2 分钟
素明诚
Full stack development

安装 Docker

apt 直接装

sudo apt install docker.io

更新您的系统 更新 Ubuntu 的包索引,确保所有系统包都是最新的。

sudo apt update
sudo apt upgrade -y

安装必要的包 安装一些必需的包,这些包允许 apt 通过 HTTPS 使用仓库

sudo apt install apt-transport-https ca-certificates curl software-properties-common

添加 Docker 的官方 GPG 密钥 确保下载的软件包是从 Docker 的官方源获取的。

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

设置 Docker 仓库apt 源列表添加 Docker 仓库。

sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"

安装 Docker CE (Community Edition) 更新 apt 包索引,然后安装 Docker CE。

sudo apt update
sudo apt install docker-ce

验证 Docker 安装 检查 Docker 是否正确安装,并运行 hello-world 容器作为测试。

sudo systemctl status docker
sudo docker run hello-world

安装 Docker Compose

下载 Docker Compose 访问 Docker Compose 的 GitHub 发布页面 查找最新版本的 Docker Compose。使用 curl 下载适用于 Linux 的二进制文件。

sudo curl -L "https://github.com/docker/compose/releases/download/v2.11.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

请根据需要替换 v2.11.2 为最新版本号。

添加执行权限 为下载的二进制文件添加执行权限。

sudo chmod +x /usr/local/bin/docker-compose

验证安装 检查 Docker Compose 是否已成功安装并可以运行。

docker-compose --version

配置 Docker 无需 sudo

默认情况下,运行 Docker 命令需要 sudo。如果您想让普通用户也能运行 Docker 命令而无需使用 sudo,可以将用户添加到 Docker 组

sudo usermod -aG docker ${USER}

之后,您需要注销并重新登录。