已合并
cherry-pick from develop_0708_release to release_v0.1.1 #362
陈卉创建于 7月3日
cherry-pick from develop_0708_release to release_v0.1.1 #362
已合并
共 3 个文件变更+66-26
| @@ -16,6 +16,9 @@ from .table_def import TableDefinition, ColumnDefinition, IndexDefinition | |||
| 16 | 16 | ||
| 17 | logger = get_logger(__name__) | 17 | logger = get_logger(__name__) |
| 18 | 18 | ||
| 19 | +# 定期回收池内连接,应小于 MySQL wait_timeout / 中间层 idle 超时。 | ||
| 20 | +_DEFAULT_POOL_RECYCLE_SECONDS = 1800 | ||
| 21 | + | ||
| 19 | 22 | ||
| 20 | class Base(DeclarativeBase): | 23 | class Base(DeclarativeBase): |
| 21 | pass | 24 | pass |
| @@ -48,7 +51,9 @@ class SQLAlchemyHandler(DBHandler): | |||
| 48 | self.engine = create_async_engine( | 51 | self.engine = create_async_engine( |
| 49 | self.database_url, | 52 | self.database_url, |
| 50 | echo=False, | 53 | echo=False, |
| 51 | - connect_args=self.connect_args | 54 | + connect_args=self.connect_args, |
| 55 | + pool_pre_ping=True, | ||
| 56 | + pool_recycle=_DEFAULT_POOL_RECYCLE_SECONDS, | ||
| 52 | ) | 57 | ) |
| 53 | self.session_factory = async_sessionmaker( | 58 | self.session_factory = async_sessionmaker( |
| 54 | self.engine, class_=AsyncSession, expire_on_commit=False | 59 | self.engine, class_=AsyncSession, expire_on_commit=False |
| @@ -172,18 +172,28 @@ class K8sServiceHandler: | |||
| 172 | delete_grace_period: int = 30, | 172 | delete_grace_period: int = 30, |
| 173 | delete_timeout: float = 120.0, | 173 | delete_timeout: float = 120.0, |
| 174 | delete_poll_interval: float = 1.0, | 174 | delete_poll_interval: float = 1.0, |
| 175 | + mount_type: Optional[str] = None, | ||
| 176 | + pvc: Optional[str] = None, | ||
| 175 | nfs_server: Optional[str] = None, # NFS 服务器地址 | 177 | nfs_server: Optional[str] = None, # NFS 服务器地址 |
| 176 | nfs_path: Optional[str] = None, # NFS 共享路径 | 178 | nfs_path: Optional[str] = None, # NFS 共享路径 |
| 177 | - nfs_mount_path: Optional[str] = None, # 容器内挂载路径 | 179 | + mount_path: Optional[str] = None, # 容器内挂载路径 |
| 178 | mode: str = "product", # 运行环境模式:支持 dev / product 两种值 | 180 | mode: str = "product", # 运行环境模式:支持 dev / product 两种值 |
| 179 | node_name: Optional[str] = None, # 强制调度到指定节点 | 181 | node_name: Optional[str] = None, # 强制调度到指定节点 |
| 180 | ): | 182 | ): |
| 181 | if not containers: | 183 | if not containers: |
| 182 | raise ValueError("containers must not be empty") | 184 | raise ValueError("containers must not be empty") |
| 185 | + | ||
| 183 | ports = [int(c.port) for c in containers] | 186 | ports = [int(c.port) for c in containers] |
| 184 | if len(set(ports)) != len(ports): | 187 | if len(set(ports)) != len(ports): |
| 185 | raise ValueError("container ports must be unique in one pod") | 188 | raise ValueError("container ports must be unique in one pod") |
| 186 | 189 | ||
| 190 | + if mount_type == "nfs": | ||
| 191 | + if not nfs_server or not nfs_path or not mount_path: | ||
| 192 | + raise ValueError("nfs_server, nfs_path and mount_path are required when mount_type is 'nfs'") | ||
| 193 | + elif mount_type == "pvc": | ||
| 194 | + if not pvc: | ||
| 195 | + raise ValueError("pvc is required when mount_type is 'pvc'") | ||
| 196 | + | ||
| 187 | self._containers = list(containers) | 197 | self._containers = list(containers) |
| 188 | self._name_prefix = pod_name if pod_name else self._sanitize_prefix(name_prefix) | 198 | self._name_prefix = pod_name if pod_name else self._sanitize_prefix(name_prefix) |
| 189 | self._namespace = namespace | 199 | self._namespace = namespace |
| @@ -195,9 +205,11 @@ class K8sServiceHandler: | |||
| 195 | self._delete_grace_period = int(delete_grace_period) | 205 | self._delete_grace_period = int(delete_grace_period) |
| 196 | self._delete_timeout = float(delete_timeout) | 206 | self._delete_timeout = float(delete_timeout) |
| 197 | self._delete_poll_interval = float(delete_poll_interval) | 207 | self._delete_poll_interval = float(delete_poll_interval) |
| 208 | + self._mount_type = mount_type | ||
| 209 | + self._pvc = pvc | ||
| 198 | self._nfs_server = nfs_server | 210 | self._nfs_server = nfs_server |
| 199 | self._nfs_path = nfs_path | 211 | self._nfs_path = nfs_path |
| 200 | - self._nfs_mount_path = nfs_mount_path | 212 | + self._mount_path = mount_path |
| 201 | self._mode = mode | 213 | self._mode = mode |
| 202 | self._node_name = node_name | 214 | self._node_name = node_name |
| 203 | self._pod_name: Optional[str] = None | 215 | self._pod_name: Optional[str] = None |
| @@ -222,17 +234,23 @@ class K8sServiceHandler: | |||
| 222 | return "".join(secrets.choice(alphabet) for _ in range(length)) | 234 | return "".join(secrets.choice(alphabet) for _ in range(length)) |
| 223 | 235 | ||
| 224 | 236 | ||
| 225 | - def _build_nfs_volume_name(cls, container_name: str, idx: int) -> str: | 237 | + def _build_nfs_volume_name(cls, name: str) -> str: |
| 226 | # K8s volume 名需符合 DNS-1123 label:小写字母数字或 '-',长度 <= 63 | 238 | # K8s volume 名需符合 DNS-1123 label:小写字母数字或 '-',长度 <= 63 |
| 227 | - sanitized = cls._NAME_INVALID_CHARS.sub("-", (container_name or "").lower()).strip("-") | 239 | + sanitized = cls._NAME_INVALID_CHARS.sub("-", (name or "").lower()).strip("-") |
| 228 | - base = sanitized or f"c{idx}" | ||
| 229 | # 预留 "nfs-" 前缀 4 字符,整体限制 63 字符 | 240 | # 预留 "nfs-" 前缀 4 字符,整体限制 63 字符 |
| 230 | - return f"nfs-{base[:59]}" | 241 | + return f"nfs-{sanitized[:59]}" |
| 231 | 242 | ||
| 232 | 243 | ||
| 233 | - def _build_host_path_volume_name(cls, container_name: str, idx: int, mount_idx: int) -> str: | 244 | + def _build_pvc_volume_name(cls, name: str) -> str: |
| 245 | + # K8s volume 名需符合 DNS-1123 label:小写字母数字或 '-',长度 <= 63 | ||
| 246 | + sanitized = cls._NAME_INVALID_CHARS.sub("-", (name or "").lower()).strip("-") | ||
| 247 | + # 预留 "nfs-" 前缀 4 字符,整体限制 63 字符 | ||
| 248 | + return f"pvc-{sanitized[:59]}" | ||
| 249 | + | ||
| 250 | + | ||
| 251 | + def _build_host_path_volume_name(cls, name: str, idx: int, mount_idx: int) -> str: | ||
| 234 | # 预留 "hp-" 前缀和索引后缀,避免同一 Pod 内多容器、多挂载重名。 | 252 | # 预留 "hp-" 前缀和索引后缀,避免同一 Pod 内多容器、多挂载重名。 |
| 235 | - sanitized = cls._NAME_INVALID_CHARS.sub("-", (container_name or "").lower()).strip("-") | 253 | + sanitized = cls._NAME_INVALID_CHARS.sub("-", (name or "").lower()).strip("-") |
| 236 | base = sanitized or f"c{idx}" | 254 | base = sanitized or f"c{idx}" |
| 237 | suffix = f"-{idx}-{mount_idx}" | 255 | suffix = f"-{idx}-{mount_idx}" |
| 238 | return f"hp-{base[: 63 - len('hp-') - len(suffix)]}{suffix}" | 256 | return f"hp-{base[: 63 - len('hp-') - len(suffix)]}{suffix}" |
| @@ -345,28 +363,40 @@ class K8sServiceHandler: | |||
| 345 | 363 | ||
| 346 | annotations: Dict[str, str] = {} | 364 | annotations: Dict[str, str] = {} |
| 347 | volumes: List[client.V1Volume] = [] | 365 | volumes: List[client.V1Volume] = [] |
| 348 | - nfs_enabled = bool(self._nfs_server and self._nfs_path and self._nfs_mount_path) | 366 | + |
| 367 | + if self._mount_type == "nfs": | ||
| 368 | + pod_volume_name = self._build_nfs_volume_name(pod_name) | ||
| 369 | + volumes.append( | ||
| 370 | + client.V1Volume( | ||
| 371 | + name=pod_volume_name, | ||
| 372 | + nfs=client.V1NFSVolumeSource( | ||
| 373 | + server=self._nfs_server, | ||
| 374 | + path=self._nfs_path, | ||
| 375 | + ), | ||
| 376 | + ) | ||
| 377 | + ) | ||
| 378 | + elif self._mount_type == "pvc": | ||
| 379 | + pod_volume_name = self._build_pvc_volume_name(pod_name) | ||
| 380 | + volumes.append( | ||
| 381 | + client.V1Volume( | ||
| 382 | + name=pod_volume_name, | ||
| 383 | + persistent_volume_claim=client.V1PersistentVolumeClaimVolumeSource( | ||
| 384 | + claim_name=self._pvc, | ||
| 385 | + read_only=False, | ||
| 386 | + ) | ||
| 387 | + ) | ||
| 388 | + ) | ||
| 349 | 389 | ||
| 350 | pod_containers: List[client.V1Container] = [] | 390 | pod_containers: List[client.V1Container] = [] |
| 351 | for idx, spec in enumerate(self._containers): | 391 | for idx, spec in enumerate(self._containers): |
| 352 | env_list = [client.V1EnvVar(name=k, value=str(v)) for k, v in spec.env_vars.items()] | 392 | env_list = [client.V1EnvVar(name=k, value=str(v)) for k, v in spec.env_vars.items()] |
| 353 | 393 | ||
| 354 | container_volume_mounts: List[client.V1VolumeMount] = [] | 394 | container_volume_mounts: List[client.V1VolumeMount] = [] |
| 355 | - if nfs_enabled: | 395 | + if self._mount_type == "nfs" or self._mount_type == "pvc": |
| 356 | - nfs_volume_name = self._build_nfs_volume_name(spec.name, idx) | ||
| 357 | - volumes.append( | ||
| 358 | - client.V1Volume( | ||
| 359 | - name=nfs_volume_name, | ||
| 360 | - nfs=client.V1NFSVolumeSource( | ||
| 361 | - server=self._nfs_server, | ||
| 362 | - path=self._nfs_path, | ||
| 363 | - ), | ||
| 364 | - ) | ||
| 365 | - ) | ||
| 366 | container_volume_mounts.append( | 396 | container_volume_mounts.append( |
| 367 | client.V1VolumeMount( | 397 | client.V1VolumeMount( |
| 368 | - name=nfs_volume_name, | 398 | + name=pod_volume_name, |
| 369 | - mount_path=self._nfs_mount_path, | 399 | + mount_path=self._mount_path, |
| 370 | ) | 400 | ) |
| 371 | ) | 401 | ) |
| 372 | 402 | ||
| @@ -88,6 +88,8 @@ class WSServiceMessageChannel: | |||
| 88 | connect_timeout: float = 30.0, | 88 | connect_timeout: float = 30.0, |
| 89 | additional_headers: Optional[Any] = None, | 89 | additional_headers: Optional[Any] = None, |
| 90 | verify_peer: Optional[Callable[[dict], bool]] = None, | 90 | verify_peer: Optional[Callable[[dict], bool]] = None, |
| 91 | + ws_ping_interval: float = 20.0, | ||
| 92 | + ws_ping_timeout: float = 20.0, | ||
| 91 | ) -> None: | 93 | ) -> None: |
| 92 | self._fallback_port = int(target_port) if target_port is not None else None | 94 | self._fallback_port = int(target_port) if target_port is not None else None |
| 93 | self._port = self._fallback_port or 0 | 95 | self._port = self._fallback_port or 0 |
| @@ -120,6 +122,9 @@ class WSServiceMessageChannel: | |||
| 120 | # 在途 request 流式结束: is_completed 时 set | 122 | # 在途 request 流式结束: is_completed 时 set |
| 121 | self._request_done: Dict[str, asyncio.Event] = {} | 123 | self._request_done: Dict[str, asyncio.Event] = {} |
| 122 | self._last_service_id: str = "" | 124 | self._last_service_id: str = "" |
| 125 | + self._ws_ping_interval = ws_ping_interval | ||
| 126 | + self._ws_ping_timeout = ws_ping_timeout | ||
| 127 | + | ||
| 123 | logger.debug( | 128 | logger.debug( |
| 124 | "WSServiceMessageChannel: port=%s container=%s path=%s tls=%s", | 129 | "WSServiceMessageChannel: port=%s container=%s path=%s tls=%s", |
| 125 | target_port, | 130 | target_port, |
| @@ -219,7 +224,7 @@ class WSServiceMessageChannel: | |||
| 219 | raise RuntimeError("WSS 已关闭") | 224 | raise RuntimeError("WSS 已关闭") |
| 220 | if not self._ws_url: | 225 | if not self._ws_url: |
| 221 | raise RuntimeError("WebSocket URL 未设置") | 226 | raise RuntimeError("WebSocket URL 未设置") |
| 222 | - logger.info("WSS 正在连接: %s", self._ws_url) | 227 | + logger.info("WSS 正在连接: %s, ping_interval=%f, ping_timeout=%f", self._ws_url, self._ws_ping_interval, self._ws_ping_timeout) |
| 223 | # dict 直接用;回调则每次连接现取一份(如刷新链路令牌:新 nonce/新签发时间)。 | 228 | # dict 直接用;回调则每次连接现取一份(如刷新链路令牌:新 nonce/新签发时间)。 |
| 224 | hdrs = self._additional_headers | 229 | hdrs = self._additional_headers |
| 225 | if callable(hdrs): | 230 | if callable(hdrs): |
| @@ -228,8 +233,8 @@ class WSServiceMessageChannel: | |||
| 228 | websockets.connect( | 233 | websockets.connect( |
| 229 | self._ws_url, | 234 | self._ws_url, |
| 230 | open_timeout=self._connect_timeout, | 235 | open_timeout=self._connect_timeout, |
| 231 | - ping_interval=20.0, | 236 | + ping_interval=self._ws_ping_interval, |
| 232 | - ping_timeout=20.0, | 237 | + ping_timeout=self._ws_ping_timeout, |
| 233 | additional_headers=hdrs, | 238 | additional_headers=hdrs, |
| 234 | ), | 239 | ), |
| 235 | timeout=self._connect_timeout, | 240 | timeout=self._connect_timeout, |