资讯动态

Python后端部署排障08:容器重建后,报价为什么不该消失

发布时间:2026/10/30 4:40:53 来源:尧图企业网站定制
Python后端部署排障08容器重建后报价为什么不该消失服务在一个容器里跑通后最常见的错误是把它想成一台缩小的服务器代码、数据、日志和配置全部放进去删除后再从头安装。容器部署真正要建立的是可重复创建进程的能力数据则有独立的保存策略。本课把blue、green、edge以及数据库卷连接起来通过一次真实重建检查这个边界。E07答案先观察实际身份镜像里的USER可以被运行参数覆盖所以答案同时检查镜像配置、容器配置和容器内实际UID。三个结果一致才说明合同真正落地。os.access用当前进程身份检查父目录不直接写入数据库业务写入仍由API验收完成。最后还检查挂载类型和卷名防止误把容器可写层当作持久化卷。lessons/08/answer_previous.pyE07: inspect image identity, effective UID and persistent database location.importjsonimportsubprocessimportsysfrompathlibimportPath ROOTPath(__file__).resolve().parents[2]defcommand(*args):returnsubprocess.check_output(args,cwdROOT,textTrue,timeout60)defmain():statejson.loads(command(sys.executable,ops/labctl.py,status))evidence{}forservicein(app_blue,app_green):cidstate[containers][service]containerjson.loads(command(docker,inspect,cid))[0]imagejson.loads(command(docker,image,inspect,container[Image]))[0]assertimage[Config][User]10001:10001assertcontainer[Config][User]10001:10001probe(import os,json;from pathlib import Path;pPath(os.environ[QUOTEL_DATABASE]);print(json.dumps({uid:os.getuid(),gid:os.getgid(),database:str(p),exists:p.is_file(),writable:os.access(p.parent,os.W_OK)})))runtimejson.loads(command(docker,exec,cid,python,-c,probe))assertruntime{uid:10001,gid:10001,database:/data/quotes.db,exists:True,writable:True,}mountnext(itemforitemincontainer[Mounts]ifitem[Destination]/data)assertmount[Type]volumeandmount[RW]isTrueassertmount[Name]state[run_id]_dataevidence[service]{image_id:image[Id],runtime:runtime,volume:mount[Name],}print(json.dumps({status:PASS,images:evidence},indent2))if__name____main__:main()在Linux课程根目录运行python3 lessons/08/answer_previous.py。前置是已经执行up脚本不会悄悄创建缺失环境。检查失败就保留现场结合容器inspect定位原因不能把断言删掉来获得一份漂亮的PASS。镜像ID随构建发生变化是正常现象数字UID和数据库路径才是本课要求固定的合同。三种端口、两种地址空间app_blue与app_green都在各自容器的8000端口监听它们不会冲突因为每个容器拥有独立网络命名空间。Compose默认网络提供服务名解析edge连接app_blue:8000就能到达blue。这里的8000是应用内部端口与宿主上供个人实验使用的高端口没有必然关系。edge内部监听8080和8443Compose把它们映射到宿主127.0.0.1上的两个动态高端口。宿主之外的机器不能直接连接这个回环绑定读者通过SSH隧道接入实验入口。同样是127.0.0.1从edge里访问指向edge自己从宿主里访问指向宿主从应用里访问则指向当前应用容器。排障时先说清请求发起者在哪里再讨论地址是否正确。expose用于说明应用内部端口服务间互通主要由共同网络与实际监听决定它不是宿主防火墙规则。反过来ports确实建立宿主绑定尤其要检查是否写成了0.0.0.0。我们不给两个应用分别映射宿主端口让所有客户端证据都经过edge避免“直连成功、代理失败”时读者实际测试了错误入口。完整Compose配置部署脚本写入独立的env文件填好本次项目名、两镜像名、Nginx digest、端口和runtime绝对目录。以下配置使用变量缺失即报错的写法防止空字符串落成意外默认值。共享配置通过YAML锚点表达blue和green使用不同镜像数据库位置与运行限制一致。deploy/compose.ymlname:${QUOTEOPS_PROJECT:?Set QUOTEOPS_PROJECT}x-app:appuser:10001:10001init:trueenvironment:QUOTEL_DATABASE:/data/quotes.dbQUOTEL_ENV:prodQUOTEL_LOG_LEVEL:INFOvolumes:-data:/dataexpose:-8000read_only:truetmpfs:-/tmp:rw,noexec,nosuid,size16m,mode1777cap_drop:-ALLsecurity_opt:-no-new-privileges:truemem_limit:256mcpus:0.75pids_limit:64stop_grace_period:15srestart:unless-stoppedhealthcheck:test:[CMD,python,-c,import urllib.request; urllib.request.urlopen(http://127.0.0.1:8000/ready,timeout2).read()]interval:5stimeout:3sretries:3start_period:10slogging:loggingdriver:json-fileoptions:max-size:5mmax-file:3services:app_blue::*appimage:${QUOTEOPS_IMAGE_BLUE:?Set QUOTEOPS_IMAGE_BLUE}app_green::*appimage:${QUOTEOPS_IMAGE_GREEN:?Set QUOTEOPS_IMAGE_GREEN}edge:image:${QUOTEOPS_NGINX_IMAGE:?Set QUOTEOPS_NGINX_IMAGE}entrypoint:[nginx,-g,daemon off;]ports:-127.0.0.1:${QUOTEOPS_HTTP_PORT:?Set QUOTEOPS_HTTP_PORT}:8080-127.0.0.1:${QUOTEOPS_HTTPS_PORT:?Set QUOTEOPS_HTTPS_PORT}:8443volumes:-./nginx.conf:/etc/nginx/nginx.conf:ro-${QUOTEOPS_RUNTIME_DIR:?Set QUOTEOPS_RUNTIME_DIR}:/etc/nginx/runtime:roread_only:truetmpfs:-/var/cache/nginx:rw,noexec,nosuid,size16m-/var/run:rw,noexec,nosuid,size1msecurity_opt:-no-new-privileges:truemem_limit:128mcpus:0.5pids_limit:64stop_grace_period:15srestart:unless-stoppeddepends_on:app_blue:condition:service_healthyapp_green:condition:service_healthylogging:*loggingvolumes:data:networks:default:ipam:config:-subnet:${QUOTEOPS_SUBNET:?Set QUOTEOPS_SUBNET after conflict checks}name决定Compose项目命名空间。同一份文件在两个实验中使用不同project默认网络与命名卷也会不同清理时才能严格限定本次资源。不要写固定的全局container_name也不要在多次实验中共用一个随意命名的卷。状态文件记录本次资源实际容器ID通过Compose查询因为重建之后ID必然改变。read_only限制根文件系统写入/tmp单独给一个16MiB tmpfs/data是业务允许持久写入的位置。限制内存、CPU与PID是为了把资源故障关在小实验内并不意味着这些数值适合所有生产流量。日志采用json-file并限制单文件大小和保留数stdout不会无限增长。Nginx的配置与证书通过只读挂载提供容器不能反向修改宿主源文件。迁移为什么要在应用启动前显式执行两个应用共享一个schema时不应各自启动就抢着改表。部署脚本先创建本次卷由一次性初始化容器以root把数据目录属主设为10001随后切到应用UID执行quoteledger.migrations最后启动blue与green。root只用于新卷初始化应用常驻进程仍保持非root身份。初始化失败必须阻止后续启动而不是靠健康检查等待一个永远不会自动出现的表。SQLite创建数据库文件需要目录可写提交事务还可能创建日志文件。因此“quotes.db是可写的”不等于应用一定能正常提交还要检查父目录权限和可用空间。另一方面迁移完成不代表运行容器使用的路径就是刚迁移的那个文件检查环境变量、卷挂载、文件位置和ready结果才能把这条证据链闭合。这套实验只有一个SQLite卷、两个单进程应用数据库会串行处理写事务。蓝绿是发布策略不是把SQLite升级成高并发分布式数据库的方式。真实负载需要根据锁等待、写入吞吐、备份与高可用要求选择数据库。这里保留单文件库是为了让数据生命周期与应用生命周期的差别更容易观察。running、healthy、unhealthy分别说明什么running描述容器主进程仍在运行。healthy表示最近的健康检查通过unhealthy表示连续检查失败达到阈值。Compose里的restart: unless-stopped主要针对进程退出与Docker守护进程重启的情境单凭unhealthy不会自动把一个仍在运行的进程杀掉再启动。把两种机制混成一件事会让人误以为已经获得了故障自愈。我们用Python访问容器内的/ready作为健康检查避免为一个curl命令额外扩充应用镜像。启动宽限、间隔、超时和重试次数共同影响发现时间因此一次手动HTTP失败与容器健康状态可能短暂不一致。depends_on让edge在初次启动时等待两应用健康但它不是持续故障转移控制器green后来变坏时Compose不会替你决定是否把流量切回blue。用重建证明持久化而不是看卷列表猜测一次有效实验应先创建报价并保存完整返回值然后仅重建当前提供流量的应用容器确认新旧容器ID不同。等新应用ready之后从edge再次查询原ID要求金额、版本、状态与创建时完全相同。这样同时证明卷被重新挂载、schema可读、业务数据没有落在旧容器层里。容器重建还会带来一个容易忽略的网络细节Nginx的静态upstream服务名通常在加载配置时解析。新容器可能得到新IPedge若继续使用旧地址就会出现502。实验答案会在新应用ready后调用reload_edge先执行nginx -t再reload并观察旧worker全部退出、新worker已经出现随后才做入口检查。这个动作修复的是代理目标地址不是“让数据库恢复”故障归因必须保留层次。信号发送成功不代表交接结束下一课会用真实竞态说明这个差别。python3 ops/labctl.py status python3 lessons/08/answer_previous.py python3 ops/labctl.py check运行重建练习时可以观察容器ID变化不能删除卷来“让环境更干净”。compose down与compose down -v对数据的后果不同课程最终清理由labctl down根据本次登记资源执行按精确project标签停止并移除容器再删除本次网络、卷与镜像。这个清理入口不依赖Compose配置仍能解析因此配置出错时也有恢复路径。个人练习结束应导出需要保留的证据再清理自己的卷它不是生产备份策略下一阶段会专门演示SQLite备份与恢复。一次真实的网络创建失败本轮远端首次创建Compose网络时Docker报告默认地址池已经分配完。机器上存在其他业务网络删除它们或全局prune都不属于本课程权限也无法证明那些网络没有用途。修改daemon.json再重启Docker同样会影响整台宿主。正确处理先停在创建网络这个阶段记录错误确认本次是否留下容器、卷或镜像再只清理本次登记资源。部署脚本增加一个只读选网步骤查询全部Docker网络的IPAM配置并读取ip -json route show table all里的宿主路由选择与它们均无重叠的私有/24写入本次QUOTEOPS_SUBNET。Compose只为自己的default网络使用该网段。这里的“私有地址”并不意味着可以随便挑公司VPN、虚拟机和内网可能已经使用相同地址必须对实际路由检查而不是照抄文章里的示例。选择结果与占用证据一同进入报告网络也仍由本次project命名和清理。若候选范围全部冲突脚本明确失败让操作者重新做网络规划不继续扩大权限修改宿主全局配置。这个例子说明隔离不只体现在容器名称还包括地址范围与清理范围排障中的修复应尽量作用于当前问题需要的最小范围。新增子网后业务容器地址可能与之前尝试不同命令和教程因此继续使用服务名和状态输出不把容器IP写进代码。固定IP容易让第一次成功的实验变成后续部署的隐含依赖。读者应记住的是服务名、网络归属和命名空间三者的关系而不是某一台服务器临时分配出来的数字。E08换掉进程保留同一张报价创建一张总额4397分的报价保存完整响应与当前应用容器ID。只对当前服务执行force-recreate等待新进程ready再测试并reload Nginx观察worker交接屏障通过。断言容器ID已经变化原报价返回值完全不变。输出旧ID、新ID、报价ID、金额与新旧worker PID说明为什么这些证据比“docker volume ls里还有一项”更有说服力。下一课给出完整答案并沿着edge到应用这段链路制造502与504。到那时容器在运行不再等于请求能成功你需要把代理连接失败与等待响应超时分开。参考Compose服务配置、Compose网络、Docker volumes、容器重启策略。本课完整模块只选择不重叠的实验网络choose_subnet首先读取Docker网络和宿主全部路由然后用ipaddress判断两个网段是否重叠。不能只比较网段字符串10.253.240.0/24虽然不等于10.253.0.0/16地址却已经包含在后者中。候选池有限全部冲突时抛出错误。返回的第二个值用于记录选网依据具体部署资源仍由第17课给出的labctl统一登记。该模块不会修改现有网络或宿主路由。ops/network_plan.py仅为本次bridge选择未重叠子网不改Docker全局地址池或宿主路由。importipaddressimportjsonfromcommonimportrundefchoose_subnet():occupied[]idsrun([docker,network,ls,-q]).stdout.split()ifids:networksjson.loads(run([docker,network,inspect,*ids]).stdout)fornetworkinnetworks:foriteminnetwork.get(IPAM,{}).get(Config,[])or[]:ifitem.get(Subnet):occupied.append(ipaddress.ip_network(item[Subnet],strictFalse))forrouteinjson.loads(run([ip,-json,route,show,table,all]).stdout):valueroute.get(dst,)ifvalueandvalue!default:try:occupied.append(ipaddress.ip_network(value,strictFalse))exceptValueError:continueforsecondin[253,252,251,250,249]:forthirdinrange(240,256):candidateipaddress.ip_network(f10.{second}.{third}.0/24)ifall(net.version!4ornotcandidate.overlaps(net)fornetinoccupied):returnstr(candidate),list(map(str,occupied))raiseRuntimeError(No nonoverlapping private /24 in course candidate pool; inspect network plan)课程目录《Linux、网络与后端部署排障从“接口能跑”走到“服务能维护”》 · 上一篇第07篇 · 下一篇第09篇

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑