Skip to content

Commit

Permalink
add module of web, support crud of config server.
Browse files Browse the repository at this point in the history
  • Loading branch information
bigtutu authored and bigtutu committed Dec 9, 2024
1 parent 8f0ae4c commit 8a10238
Show file tree
Hide file tree
Showing 26 changed files with 1,195 additions and 32 deletions.
8 changes: 4 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*.webm
*.m4a
*.mp3
*.png
example/*.png

# Byte-compiled / optimized / DLL files
.idea/
Expand Down Expand Up @@ -36,7 +36,7 @@ share/python-wheels/
MANIFEST

# PyInstaller
# Usually these files are written by a python script from a template
# Usually these files are written by a python script from a templates
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
Expand All @@ -45,7 +45,7 @@ MANIFEST
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
# Unit example / coverage reports
htmlcov/
.tox/
.nox/
Expand Down Expand Up @@ -164,7 +164,7 @@ dmypy.json
cython_debug/

# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# JetBrains specific templates is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
Expand Down
84 changes: 74 additions & 10 deletions ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,16 @@

## 简介

Redis Config Center 是一个基于Redis实现的简易版配置中心,旨在支持不同节点动态分配不同的配置组。它模仿了Nacos的设计理念,提供了配置的发布、自动更新,获取,配置组隔离等功能,并且能够根据当前节点的机器ID自动分配对应的配置组。

### 实现背景
我们有一批能动态扩缩容的集群节点,需要为不同节点设置不同的配置组信息,除了支持常规的配置组动态刷新外,我们还希望能动态上下线配置组,并在上下线过程中,为节点自动分配新的配置组。
Redis Config Center 是一个基于Redis实现的简易版配置中心,旨在均衡分配有限的配置组给集群的不同节点。它模仿了Nacos的设计理念,提供了配置的发布、自动更新,获取,配置组隔离等功能。当指定的配置组不存在时,能够根据当前节点的机器IP自动分配对应的配置组。

## 特性

- **配置管理**:通过Redis进行配置的存储和管理。
- **动态刷新**:客户端定时从服务器拉取最新的配置。
- **配置组隔离**:基于配置组隔离不同配置,可在环境变量中指定配置组。
- **IP路由/配置组动态分配**:当指定的配置组不存在或者被删除时,基于IP地址自动重新分配配置组,保证服务至少有一个配置组可用且无需重启。
- **可扩展性强**:核心实现为两个类 redis_config_server.py 与 redis_config_client.py 。代码仅100行左右,高可读性与可扩展性。
- **异步非阻塞**: python 协程实现,非阻塞。
- **可扩展性强**:核心代码仅100行左右,高可读性与可扩展性

## 安装与使用

Expand All @@ -30,10 +27,25 @@ Redis Config Center 是一个基于Redis实现的简易版配置中心,旨在
pip install -r requirements.txt
```

项目启动环境变量配置:
```python
# 必须配置:
import os
REDIS_URL = os.getenv("REDIS_URL", "localhost")
REDIS_PORT = int(os.getenv("REDIS_PORT", 6379))
REDIS_DB = int(os.getenv("REDIS_DB", 0))
REDIS_PASSWORD = os.getenv("REDIS_PASSWORD", None)

# 可选配置:
DEFAULT_CONFIG_GROUP = os.getenv("DEFAULT_CONFIG_GROUP", "default")
REDIS_CONFIG_CENTER_TYPE = os.getenv("REDIS_CONFIG_CENTER_TYPE", "PRODUCTION")

```


### 使用方法
### 开始使用

#### 发布配置 (服务端)
#### 服务端:发布配置

基于 `redis_config_server.py` 发布配置:

Expand All @@ -43,7 +55,7 @@ from redis_config_server import config_server, ConfigGroup

async def test():
await config_server.insert_config_group(
ConfigGroup(group_name="default", group_version="1", config_dict={"key1": "value1"}))
ConfigGroup(group_name="default", group_version=1, config_dict={"key1": "value1"}))
# ... 插入其他配置组 ...

print(await config_server.get_config_group("default"))
Expand All @@ -53,7 +65,7 @@ if __name__ == "__main__":
asyncio.run(test())
```

#### 获取配置 (客户端)
#### 客户端:获取配置

基于 `redis_config_client.py` 启动配置中心客户端,并获取配置:

Expand All @@ -76,8 +88,60 @@ if __name__ == "__main__":

```

*完整测试用例可参考:redis_config_test.py*
#### 客户端:集成FastAPI
```python
from contextlib import asynccontextmanager

import uvicorn
from fastapi import FastAPI
from redis_config_client import config_client

@asynccontextmanager
async def lifespan(app: FastAPI):
await config_client.start()
yield

app = FastAPI(lifespan=lifespan)

if __name__ == "__main__":
uvicorn.run(app, host="0.0.0.0", port=8089)

```

*完整测试用例可参考:example/all_it.py*

## 服务端后台管理平台

我们同样提供了一个功能丰富的Web服务端配置管理平台,其代码位于`web`目录。可以通过直接运行`web/main.py`来启动应用,或者使用`deploy.sh`脚本将项目打包为Docker镜像,以便于部署和访问。
(该管理后台实现全部是基于redis_config_server.py)

### 核心功能

- **配置管理**:通过直观的Web界面轻松进行配置的发布、修改与删除。
- **数据迁移**:支持配置数据的导入、导出及迁移,确保数据操作的灵活性。
- **便捷部署**:提供Docker容器化选项,简化了跨环境部署流程。

### Web服务端快速启动

- **直接运行启动**:确保已安装Python环境后,直接执行`python web/main.py`启动服务。
- **Docker部署启动**:在项目根目录下运行`./deploy.sh`,该脚本会自动构建并启动Docker容器。

### 功能演示

以下是部分核心功能的截图展示:

![img_1.png](md/img_1.png)

![img_2.png](md/img_2.png)

![img_3.png](md/img_3.png)

![img_4.png](md/img_4.png)

![img_5.png](md/img_5.png)

## 实现背景
我们有一批有限的配置组(主要是一堆有调用次数限制的apikey,为了不超过限额,只能均衡分配给不同节点使用),以及一批能动态扩缩容的集群节点,需要把配置组均衡分配给不同的节点使用。除了支持常规的配置组动态刷新外,我们还希望能动态上下线配置组,并在上下线过程中,为节点自动分配新的配置组。基于此背景实现并开源了本项目,期待有更多的小伙伴使用和支持该项目~

## 贡献指南

Expand Down
14 changes: 7 additions & 7 deletions redis_config_test.py → example/all_happy_path.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from redis_config_server import config_server, ConfigGroup
from redis_config_client import config_client


# 主流程测试
if __name__ == "__main__":
async def test():
await config_server.delete_config_group("default")
Expand All @@ -14,16 +14,16 @@ async def test():

# 插入服务端配置
await config_server.insert_config_group(
ConfigGroup(group_name="default", group_version="1", config_dict={"default_key": "default_value"})
ConfigGroup(group_name="default", group_version=1, config_dict={"default_key": "default_value"})
)
await config_server.insert_config_group(
ConfigGroup(group_name="group_1", group_version="1", config_dict={"key1": "value1"})
ConfigGroup(group_name="group_1", group_version=1, config_dict={"key1": "value1"})
)
await config_server.insert_config_group(
ConfigGroup(group_name="group_3", group_version="1", config_dict={"key3": "value3"})
ConfigGroup(group_name="group_3", group_version=1, config_dict={"key3": "value3"})
)
await config_server.insert_config_group(
ConfigGroup(group_name="group_2", group_version="1", config_dict={"key2": "value2"})
ConfigGroup(group_name="group_2", group_version=1, config_dict={"key2": "value2"})
)

# 获取服务端当前配置组信息
Expand All @@ -40,7 +40,7 @@ async def test():
print(f"客户端当前配置为:{cur_config_group}, cur_config_val: {cur_config_val}")

# 模拟服务端更新,客户端同步更新配置
await config_server.insert_config_group(ConfigGroup(group_name=cur_config_group.group_name, group_version="2",
await config_server.insert_config_group(ConfigGroup(group_name=cur_config_group.group_name, group_version=2,
config_dict=cur_config_group.config_dict))
await asyncio.sleep(6)
cur_config_group = config_client.get_config_group()
Expand All @@ -56,7 +56,7 @@ async def test():

# 模拟服务端新增配置组,客户端重新分配配置组
await config_server.insert_config_group(
ConfigGroup(group_name="group_4", group_version="1", config_dict={"key4": "value4"})
ConfigGroup(group_name="group_4", group_version=1, config_dict={"key4": "value4"})
)
await asyncio.sleep(6)
cur_config_group = config_client.get_config_group()
Expand Down
17 changes: 17 additions & 0 deletions example/client_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import asyncio
from redis_config_client import config_client

# 客户端快速开始示例:
async def test():
await config_client.start()
await asyncio.sleep(6) # 等待足够的时间让配置刷新完成

cur_config_group = config_client.get_config_group()
test_val = config_client.get_config("test_key")

print(f"客户端当前配置组信息为:{cur_config_group}")
print(f"客户端当前配置'test_key'的值为:{test_val}")


if __name__ == "__main__":
asyncio.run(test())
32 changes: 32 additions & 0 deletions example/client_fastapi_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
from contextlib import asynccontextmanager
import uvicorn
from fastapi import FastAPI
from redis_config_client import config_client

# FastAPI 集成 redis_config_client 示例:
# 1. 绑定 config_client.start() 到 FastAPI 的 lifespan,在应用启动时初始化配置客户端。
# 2. 在 config_client.start() 成功启动后,项目的任何地方都可以直接调用
# redis_config_client.ConfigClient.get_config() 方法获取配置。

@asynccontextmanager
async def lifespan(app: FastAPI):
# 启动配置客户端
await config_client.start()
yield

# 创建 FastAPI 应用实例,并绑定 lifespan 函数
app = FastAPI(lifespan=lifespan)

# 示例路由,您可以根据需要添加更多路由和功能
@app.get("/")
async def read_root():
return {
"code": "200",
"message": "Hello, Redis Config Center!",
"data": config_client.get_config_group()
}

if __name__ == "__main__":
# 使用 uvicorn 运行 FastAPI 应用
# 可直接访问 http://127.0.0.1:8089/ 查看当前最新配置
uvicorn.run(app, host="127.0.0.1", port=8089)
15 changes: 15 additions & 0 deletions example/server_example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import asyncio
from redis_config_server import config_server, ConfigGroup

# 服务端快速开始示例:
async def test():
await config_server.insert_config_group(
ConfigGroup(group_name="default", group_version=1, config_dict={"key1": "value1"}))
# ... 插入其他配置组 ...

print(await config_server.get_config_group("default"))
# ... 获取其他配置组 ...


if __name__ == "__main__":
asyncio.run(test())
Binary file added md/img_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added md/img_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added md/img_3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added md/img_4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added md/img_5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 4 additions & 4 deletions redis_config_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
REDIS_PORT = int(os.getenv("REDIS_PORT", 6379))
REDIS_DB = int(os.getenv("REDIS_DB", 0))
REDIS_PASSWORD = os.getenv("REDIS_PASSWORD", None)
DEFAULT_CONFIG_GROUP = os.getenv("MACHINE_ID", "default")
DEFAULT_CONFIG_GROUP = os.getenv("DEFAULT_CONFIG_GROUP", "default")

# 配置 Redis 客户端
import valkey.asyncio as avalkey
Expand All @@ -35,14 +35,14 @@

class ConfigGroup(BaseModel):
group_name: str
group_version: str
group_version: int
config_dict: Optional[dict] = None


class ConfigClient:
def __init__(self, config_server_name: str, refresh_time: int = 5):
self.config_server_name = config_server_name
self.config_group = ConfigGroup(group_name=DEFAULT_CONFIG_GROUP, group_version="-1", config_dict={})
self.config_group = ConfigGroup(group_name=DEFAULT_CONFIG_GROUP, group_version=-1, config_dict={})
self.refresh_time = refresh_time
self._refresh_task = None

Expand Down Expand Up @@ -106,5 +106,5 @@ async def start(self):
logger.info("[ConfigClient] Redis client config center started.")


config_client = ConfigClient(config_server_name="redis_config_center", refresh_time=5)
config_client = ConfigClient(config_server_name="rcc::config::redis_config_center", refresh_time=5)

28 changes: 22 additions & 6 deletions redis_config_server.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import logging
import os
from typing import Optional, Any
from typing import Optional

from pydantic import BaseModel

Expand All @@ -23,7 +23,7 @@
port=REDIS_PORT,
db=REDIS_DB,
password=REDIS_PASSWORD,
max_connections=1
max_connections=10
)
except Exception as e:
logger.error(f"[Redis Config Center] 初始化 Redis 客户端失败: {e}")
Expand All @@ -32,7 +32,7 @@

class ConfigGroup(BaseModel):
group_name: str
group_version: str
group_version: int
config_dict: Optional[dict] = None


Expand All @@ -42,10 +42,17 @@ def __init__(self, config_server_name: str):

async def insert_config_group(self, config_group: ConfigGroup):
try:
# 如果配置组存在,则增加版本号
existing_config_group = await self.get_config_group(config_group.group_name)
if existing_config_group:
config_group.group_version = existing_config_group.group_version + 1
else:
config_group.group_version = 0

await redis_client.hset(self.config_server_name, config_group.group_name, config_group.model_dump_json())
logger.info(f"[ConfigServer] 配置组 {config_group.group_name} (添加/更新)")
logger.info(f"[ConfigServer] 配置组 {config_group.group_name} 已(添加/更新)")
except Exception as e:
logger.error(f"[ConfigServer] (添加/更新) 配置组失败: {e}")
logger.error(f"[ConfigServer](添加/更新) 配置组失败: {e}")

async def get_config_group(self, group_name: str) -> Optional[ConfigGroup]:
try:
Expand All @@ -64,6 +71,15 @@ async def delete_config_group(self, group_name: str):
except Exception as e:
logger.error(f"[ConfigServer] 删除配置组失败: {e}")

async def get_all_config_groups(self):
try:
keys = sorted(await redis_client.hkeys(self.config_server_name))
groups = [await config_server.get_config_group(key.decode('utf-8')) for key in keys]
return [g for g in groups if g is not None]
except Exception as e:
logger.error(f"[ConfigServer] 获取所有配置组失败: {e}")
return []


config_server = ConfigServer(config_server_name="redis_config_center")
config_server = ConfigServer(config_server_name="rcc::config::redis_config_center")

5 changes: 4 additions & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
pydantic==2.7.4
pydantic_core==2.18.4
libvalkey==4.0.0
valkey==6.0.2
valkey==6.0.2

fastapi==0.111.0
fastapi-cli==0.0.4
Loading

0 comments on commit 8a10238

Please sign in to comment.