已合并
cherry-pick from develop_0708_release to release_v0.1.1 #362
cherry-pick from develop_0708_release to release_v0.1.1 #362
已合并
陈卉创建于 7月3日
3 个文件变更+66-26
@@ -16,6 +16,9 @@ from .table_def import TableDefinition, ColumnDefinition, IndexDefinition
16 16 
17logger = get_logger(__name__)17logger = get_logger(__name__)
18 18 
19+# 定期回收池内连接,应小于 MySQL wait_timeout / 中间层 idle 超时。
20+_DEFAULT_POOL_RECYCLE_SECONDS = 1800
21+ 
19 22 
20class Base(DeclarativeBase):23class Base(DeclarativeBase):
21 pass24 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_args54+ 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=False59 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 = namespace199 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_server210 self._nfs_server = nfs_server
199 self._nfs_path = nfs_path211 self._nfs_path = nfs_path
200- self._nfs_mount_path = nfs_mount_path212+ self._mount_path = mount_path
201 self._mode = mode213 self._mode = mode
202 self._node_name = node_name214 self._node_name = node_name
203 self._pod_name: Optional[str] = None215 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 @classmethod236 @classmethod
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:小写字母数字或 '-',长度 <= 63238 # 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 @classmethod243 @classmethod
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+ @classmethod
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 None94 self._fallback_port = int(target_port) if target_port is not None else None
93 self._port = self._fallback_port or 095 self._port = self._fallback_port or 0
@@ -120,6 +122,9 @@ class WSServiceMessageChannel:
120 # 在途 request 流式结束: is_completed 时 set122 # 在途 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_headers229 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,