原创 冯若航 非法加冯 2023-12-04 08:17 北京
前言:这篇文章是19年1月写的,四年过去了,涉及到数据库与容器的利弊权衡依然成立。这里进行细微调整后重新发出。明天我会发布一篇《数据库是否应当放入K8S中?》,那么今天就先用这篇老文来预热一下吧。
对于无状态的应用服务而言,容器是一个相当完美的开发运维解决方案。然而对于带持久状态的服务 —— 数据库来说,事情就没有那么简单了。生产环境的数据库是否应当放入容器中,仍然是一个充满争议的问题。
站在开发者的角度上,我非常喜欢Docker,并相信容器也许是未来软件开发部署运维的标准方式。但站在DBA的立场上,我认为就目前而言,将生产环境数据库放入Docker / K8S 中仍然是一个馊主意。
让我们先来看一看Docker对自己的描述。
Docker用于形容自己的词汇包括:轻量,标准化,可移植,节约成本,提高效率,自动,集成,高效运维。这些说法并没有问题,Docker在整体意义上确实让开发和运维都变得更容易了。因而可以看到很多公司都热切地希望将自己的软件与服务容器化。但有时候这种热情会走向另一个极端:将一切软件服务都容器化,甚至是生产环境的数据库。
容器最初是针对无状态的应用而设计的,在逻辑上,容器内应用产生的临时数据也属于该容器的一部分。用容器创建起一个服务,用完之后销毁它。这些应用本身没有状态,状态通常保存在容器外部的数据库里,这是经典的架构与用法,也是容器的设计哲学。
但当用户想把数据库本身也放到容器中时,事情就变得不一样了:数据库是有状态的,为了维持这个状态不随容器停止而销毁,数据库容器需要在容器上打一个洞,与底层操作系统上的数据卷相联通。这样的容器,不再是一个能够随意创建,销毁,搬运,转移的对象,而是与底层环境相绑定的对象。因此,传统应用使用容器的诸多优势,对于数据库容器来说都不复存在。
让软件跑起来,和让软件可靠地运行是两回事。数据库是信息系统的核心,在绝大多数场景下属于关键(Critical)应用,Critical Application可按字面解释,就是出了问题会要命的应用。这与我们的日常经验相符:Word/Excel/PPT这些办公软件如果崩了强制重启即可,没什么大不了的;但正在编辑的文档如果丢了、脏了、乱了,那才是真的灾难。数据库亦然,对于不少公司,特别是互联网公司来说,如果数据库被删了又没有可用备份,基本上可以宣告关门大吉了。
可靠性(Reliability)是数据库最重要的属性。可靠性是系统在困境(adversity)(硬件故障、软件故障、人为错误)中仍可正常工作(正确完成功能,并能达到期望的性能水准)的能力。可靠性意味着容错(fault-tolerant)与韧性(resilient),它是一种安全属性,并不像性能与可维护性那样的活性属性直观可衡量。它只能通过长时间的正常运行来证明,或者某一次故障来否证。很多人往往会在平时忽视安全属性,而在生病后,车祸后,被抢劫后才追悔莫及。安全生产重于泰山,数据库被删,被搅乱,被脱库后再捶胸顿足是没有意义的。
回头再看一看Docker对自己的特性描述中,并没有包含“可靠”这个对于数据库至关重要的属性。
如前所述,可靠性并没有一个很好的衡量方式。只有通过长时间的正确运行,我们才能对一个系统的可靠性逐渐建立信心。在裸机上部署数据库可谓自古以来的实践,通过几十年的持续工作,它很好地证明了自己的可靠性。Docker虽为DevOps带来一场革命,但仅仅五年的历史对于可靠性证明而言仍然是图样图森破。对关乎身家性命的生产数据库而言还远远不够:因为还没有足够的小白鼠去趟雷。
想要提高可靠性,最重要的就是从故障中吸取经验。故障是宝贵的经验财富:它将未知问题变为已知问题,是运维知识的表现形式。社区的故障经验绝大多都基于裸机部署的假设,各式各样的故障在几十年里都已经被人们踩了个遍。如果你遇到一些问题,大概率是别人已经踩过的坑,可以比较方便地处理与解决。同样的故障如果加上一个“Docker”关键字,能找到的有用信息就要少得多。这也意味着当疑难杂症出现时,成功抢救恢复数据的概率要更低,处理紧急故障所需的时间会更长。
微妙的现实是,如果没有特殊理由,企业与个人通常并不愿意分享故障方面的经验。故障有损企业的声誉:可能暴露一些敏感信息,或者是企业与团队的垃圾程度。另一方面,故障经验几乎都是真金白银的损失与学费换来的,是运维人员的核心价值所在,因此有关故障方面的公开资料并不多。
开发关心Feature,而运维关注Bug。相比裸机部署而言,将数据库放入Docker中并不能降低硬件故障、软件错误、人为失误的发生概率。用裸机会有的硬件故障,用Docker一个也不会少。软件缺陷主要是应用Bug,也不会因为采用容器与否而降低,人为失误同理。相反,引入Docker会因为引入了额外的组件,额外的复杂度,额外的失效点,导致系统整体可靠性下降。
举个最简单的例子,dockerd守护进程崩了怎么办,数据库进程就直接歇菜了。尽管这种事情发生的概率并不高,但它们在裸机上 —— 压根不会发生。
此外,一个额外组件引入的失效点可能并不止一个:Docker产生的问题并不仅仅是Docker本身的问题。当故障发生时,可能是单纯Docker的问题,或者是Docker与数据库相互作用产生的问题,还可能是Docker与操作系统,编排系统,虚拟机,网络,磁盘相互作用产生的问题。可以参见官方PostgreSQL Docker镜像的Issue列表:https://github.com/docker-library/postgres/issues?q=。
正如《从降本增笑到降本增效》中所说,智力功率很难在空间上累加 —— 团队的智力功率往往取决于最资深几个灵魂人物的水平以及他们的沟通成本。当数据库出现问题时需要数据库专家来解决;当容器出现问题时需要容器专家来看问题;然而当你把数据库放入 Kubernetes 时,单独的数据库专家和 K8S 专家的智力带宽是很难叠加的 —— 你需要一个双料专家才能解决问题。而同时精通这两者的软件肯定要比单独的数据库专家少得多。
此外,彼之蜜糖,吾之砒霜。某些Docker的Feature,在特定的环境下也可能会变为Bug。
Docker提供了进程级别的隔离性,通常来说隔离性对应用来说是个好属性。应用看不见别的进程,自然也不会有很多相互作用导致的问题,进而提高了系统的可靠性。但隔离性对于数据库而言不一定完全是好事。
一个微妙的真实案例是在同一个数据目录上启动两个PostgreSQL实例,或者在宿主机和容器内同时启动了两个数据库实例。在裸机上第二次启动尝试会失败,因为PostgreSQL能意识到另一个实例的存在而拒绝启动;但在使用Docker的情况下因其隔离性,第二个实例无法意识到宿主机或其他数据库容器中的另一个实例。如果没有配置合理的Fencing机制(例如通过宿主机端口互斥,pid文件互斥),两个运行在同一数据目录上的数据库进程能把数据文件搅成一团浆糊。
数据库需不需要隔离性?当然需要, 但不是这种隔离性。数据库的性能很重要,因此往往是独占物理机部署。除了数据库进程和必要的工具,不会有其他应用。即使放在容器中,也往往采用独占绑定物理机的模式运行。因此Docker提供的隔离性对于这种数据库部署方案而言并没有什么意义;不过对云数据库厂商来说,这倒真是一个实用的Feature,用来搞多租户超卖妙用无穷。
数据库需要工具来维护,包括各式各样的运维脚本,部署,备份,归档,故障切换,大小版本升级,插件安装,连接池,性能分析,监控,调优,巡检,修复。这些工具,也大多针对裸机部署而设计。这些工具与数据库一样,都需要精心而充分的测试。让一个东西跑起来,与确信这个东西能持久稳定正确的运行,是完全不同的可靠性水准。
一个简单的例子是插件与包管理,PostgreSQL提供了很多实用的插件,譬如PostGIS。假如想为数据库安装该插件,在裸机上只要yum install然后create extension postgis两条命令就可以。但如果是在Docker里,按照Docker的实践原则,用户需要在镜像层次进行这个变更,否则下次容器重启时这个扩展就没了。因而需要修改Dockerfile,重新构建新镜像并推送到服务器上,最后重启数据库容器,毫无疑问,要麻烦得多。
包管理是操作系统发行版的核心问题。然而 Docker 搅乱了这一切,例如,许多 PostgreSQL 不再以 RPM/DEB 包的形式发布二进制,而是以加装扩展的 Postgres Docker 镜像分发。这就会立即产生一个显著的问题,如果我想同时使用两种,三种,或者PG生态的一百多种扩展,那么应该如何把这些散碎的镜像整合到一起呢?相比可靠的操作系统包管理,构建Docker镜像总是需要耗费更多时间与精力才能正常起效。
再比如说监控,在传统的裸机部署模式下,机器的各项指标是数据库指标的重要组成部分。容器中的监控与裸机上的监控有很多微妙的区别。不注意可能会掉到坑里。例如,CPU各种模式的时长之和,在裸机上始终会是100%,但这样的假设在容器中就不一定总是成立了。再比方说依赖/proc文件系统的监控程序可能在容器中获得与裸机上涵义完全不同的指标。虽然这类问题最终都是可解的(例如把Proc文件系统挂载到容器内),但相比简洁明了的方案,没人喜欢复杂丑陋的work around。
类似的问题包括一些故障检测工具与系统常用命令,虽然理论上可以直接在宿主机上执行,但谁能保证容器里的结果和裸机上的结果有着相同的涵义?更为棘手的是紧急故障处理时,一些需要临时安装使用的工具在容器里没有,外网不通,如果再走Dockerfile→Image→重启这种路径毫无疑问会让人抓狂。
把Docker当成虚拟机来用的话,很多工具大抵上还是可以正常工作的,不过这样就丧失了使用的Docker的大部分意义,不过是把它当成了另一个包管理器用而已。有人觉得Docker通过标准化的部署方式增加了系统的可靠性,因为环境更为标准化更为可控。这一点不能否认。私以为,标准化的部署方式虽然很不错,但如果运维管理数据库的人本身了解如何配置数据库环境,将环境初始化命令写在Shell脚本里和写在Dockerfile里并没有本质上的区别。
软件的大部分开销并不在最初的开发阶段,而是在持续的维护阶段,包括修复漏洞、保持系统正常运行、处理故障、版本升级,偿还技术债、添加新的功能等等。可维护性对于运维人员的工作生活质量非常重要。
应该说可维护性是Docker最讨喜的地方:Infrastructure as code。可以认为Docker的最大价值就在于它能够把软件的运维经验沉淀成可复用的代码,以一种简便的方式积累起来,而不再是散落在各个角落的install/setup文档。
在这一点上Docker做的相当出色,尤其是对于逻辑经常变化的无状态应用而言。Docker和K8s能让用户轻松部署,完成扩容,缩容,发布,滚动升级等工作,让Dev也能干Ops的活,让Ops也能干DBA的活(迫真)。
如果说Docker最大的优点是什么,那也许就是环境配置的标准化了。标准化的环境有助于交付变更,交流问题,复现Bug。使用二进制镜像(本质是物化了的Dockerfile安装脚本)相比执行安装脚本而言更为快捷,管理更方便。一些编译复杂,依赖如山的扩展也不用每次都重新构建了,这些都是很不错的特性。
不幸的是,数据库并不像通常的业务应用一样来来去去更新频繁,创建新实例或者交付环境本身是一个极低频的操作。同时DBA们通常都会积累下各种安装配置维护脚本,一键配置环境也并不会比Docker慢多少。因此在环境配置上Docker的优势就没有那么显著了,只能说是 Nice to have。当然,在没有专职DBA时,使用Docker镜像可能还是要比自己瞎折腾要好一些,因为起码镜像中多少沉淀了一些运维经验。
通常来说,数据库初始化之后连续运行几个月几年也并不稀奇。占据数据库管理工作主要内容的并不是创建新实例与交付环境,主要还是日常运维的部分 —— Day2 Operation。不幸的是,在这一点上Docker并没有什么优势,反而会产生不少的额外麻烦。
Docker确实能极大地简化无状态应用的日常维护工作,诸如创建销毁,版本升级,扩容等,但同样的结论能延伸到数据库上吗?
数据库容器不可能像应用容器一样随意销毁创建,重启迁移。因而Docker并不能对数据库的日常运维的体验有什么提升,真正有帮助的倒是诸如 ansible 之类的工具。而对于日常运维而言,很多操作都需要通过docker exec的方式将脚本透传至容器内执行。底下跑的还是一样的脚本,只不过用docker-exec来执行又额外多了一层包装,这就有点脱裤子放屁的意味了。
此外,很多命令行工具在和Docker配合使用时都相当尴尬。譬如docker exec会将stderr和stdout混在一起,让很多依赖管道的命令无法正常工作。以PostgreSQL为例,在裸机部署模式下,某些日常ETL任务可以用一行bash轻松搞定:
psql <src-url> -c 'COPY tbl TO STDOUT' |\psql <dst-url> -c 'COPY tdb FROM STDIN'
但如果宿主机上没有合适的客户端二进制程序,那就只能这样用Docker容器中的二进制:
xxxxxxxxxxdocker exec -it srcpg gosu postgres bash -c "psql -c \"COPY tbl TO STDOUT\" 2>/dev/null" |\ docker exec -i dstpg gosu postgres psql -c 'COPY tbl FROM STDIN;'
当用户想为容器里的数据库做一个物理备份时,原本很简单的一条命令现在需要很多额外的包装:docker套gosu套bash套pg_basebackup:
xxxxxxxxxxdocker exec -i postgres_pg_1 gosu postgres bash -c 'pg_basebackup -Xf -Ft -c fast -D - 2>/dev/null' | tar -xC /tmp/backup/basebackup
如果说客户端应用psql|pg_basebackup|pg_dump还可以通过在宿主机上安装对应版本的客户端工具来绕开这个问题,那么服务端的应用就真的无解了。总不能在不断升级容器内数据库软件的版本时每次都一并把宿主机上的服务器端二进制版本升级了吧?
另一个Docker喜欢讲的例子是软件版本升级:例如用Docker升级数据库小版本,只要简单地修改Dockerfile里的版本号,重新构建镜像然后重启数据库容器就可以了。没错,至少对于无状态的应用来说这是成立的。但当需要进行数据库原地大版本升级时问题就来了,用户还需要同时修改数据库状态。在裸机上一行bash命令就可以解决的问题,在Docker下可能就会变成这样的东西:https://github.com/tianon/docker-postgres-upgrade。
如果数据库容器不能像AppServer一样随意地调度,快速地扩展,也无法在初始配置,日常运维,以及紧急故障处理时相比普通脚本的方式带来更多便利性,我们又为什么要把生产环境的数据库塞进容器里呢?
Docker和K8s一个很讨喜的地方是很容易进行扩容,至少对于无状态的应用而言是这样:一键拉起起几个新容器,随意调度到哪个节点都无所谓。但数据库不一样,作为一个有状态的应用,数据库并不能像普通AppServer一样随意创建,销毁,水平扩展。譬如,用户创建一个新从库,即使使用容器,也得从主库上重新拉取基础备份。生产环境中动辄几TB的数据库,创建副本也需要个把钟头才能完成,也需要人工介入与检查,并逐渐放量预热缓存才能上线承载流量。相比之下,在同样的操作系统初始环境下,运行现成的拉从库脚本与跑docker run在本质上又能有什么区别 —— 时间都花在拖从库上了。
使用Docker盛放生产数据库的一个尴尬之处就在于,数据库是有状态的,而且为了建立这个状态需要额外的工序。通常来说设置一个新PostgreSQL从库的流程是,先通过pg_baseback建立本地的数据目录副本,然后再在本地数据目录上启动postmaster进程。然而容器是和进程绑定的,一旦进程退出容器也随之停止。因此为了在Docker中扩容一个新从库:要么需要先后启动pg_baseback容器拉取数据目录,再在同一个数据卷上启动postgres两个容器;要么需要在创建容器的过程中就指定定好复制目标并等待几个小时的复制完成;要么在postgres容器中再使用pg_basebackup偷天换日替换数据目录。无论哪一种方案都是既不优雅也不简洁。因为容器的这种进程隔离抽象,对于数据库这种充满状态的多进程,多任务,多实例协作的应用存在抽象泄漏,它很难优雅地覆盖这些场景。当然有很多折衷的办法可以打补丁来解决这类问题,然而其代价就是大量额外复杂度,最终受伤的还是系统的可维护性。
总的来说,Docker 在某些层面上可以提高系统的可维护性,比如简化创建新实例的操作,但它引入的新麻烦让这样的优势显得苍白无力。
性能也是人们经常关注的一个维度。从性能的角度来看,数据库的基本部署原则当然是离硬件越近越好,额外的隔离与抽象不利于数据库的性能:越多的隔离意味着越多的开销,即使只是内核栈中的额外拷贝。对于追求性能的场景,一些数据库选择绕开操作系统的页面管理机制直接操作磁盘,而一些数据库甚至会使用FPGA甚至GPU加速查询处理。
实事求是地讲,Docker作为一种轻量化的容器,性能上的折损并不大,通常不会超过 10% 。但毫无疑问的是,将数据库放入Docker只会让性能变得更差而不是更好。
容器技术与编排技术对于运维而言是非常有价值的东西,它实际上弥补了从软件到服务之间的空白,其愿景是将运维的经验与能力代码化模块化。容器技术将成为未来的包管理方式,而编排技术将进一步发展为“数据中心分布式集群操作系统”,成为一切软件的底层基础设施Runtime。当越来越多的坑被踩完后,人们可以放心大胆的把一切应用,有状态的还是无状态的都放到容器中去运行。但现在起码对于数据库而言,还只是一个美好的愿景与鸡肋的选项。
需要再次强调的是,以上讨论仅限于生产环境数据库。对于开发测试而言,尽管有基于Vagrant的虚拟机沙箱,但我也支持使用Docker —— 毕竟不是所有的开发人员都知道怎么配置本地测试数据库环境,使用Docker交付环境显然要比一堆手册简单明了得多。对于生产环境的无状态应用,甚至一些带有衍生状态的不甚重要衍生数据系统(譬如Redis缓存),Docker也是一个不错的选择。但对于生产环境的核心关系型数据库而言,如果里面的数据真的很重要,使用Docker前还是需要三思:这样做的价值到底在哪里?出了疑难杂症能Hold住吗?搞砸了这锅背得动吗?
任何技术决策都是一个利弊权衡的过程,譬如这里使用Docker的核心权衡可能就是牺牲可靠性换取可维护性。确实有一些场景,数据可靠性并不是那么重要,或者说有其他的考量:譬如对于云计算厂商来说,把数据库放到容器里混部超卖就是一件很好的事情:容器的隔离性,高资源利用率,以及管理上的便利性都与该场景十分契合。这种情况下将数据库放入Docker中,也许对他们而言就是利大于弊的。但对于更多的场景来说,可靠性往往都是优先级最高的的属性,牺牲可靠性换取可维护性通常并不是一个可取的选择。更何况也很难说运维管理数据库的工作,会因为用了Docker而轻松多少:为了安装部署一次性的便利而牺牲长久的日常运维可维护性并不是一个好主意。
综上所述,将生产环境的数据库放入容器中确实不是一个明智的选择。
明日预告
参考阅读
《Docker 的诅咒:曾以为它是终极解法,最后却是“罪大恶极”?》
原创 冯若航 非法加冯 2023-12-05 12:10 北京
数据库是否应该放入 Kubernetes / Docker 里,到今天仍然是一个充满争议的话题。k8s 作为一个先进的容器编排工具,在无状态应用管理上非常趁手;但其在处理有状态服务 —— 特别是PostgreSQL和MySQL这样的数据库时,有着本质上的局限性。
在上一篇文章《数据库放入Docker是个好主意吗?》中,我们已经讨论了容器化数据库的利弊权衡;今天我们就来聊一聊将数据库放入 K8S 中编排调度所涉及的利弊权衡 —— 并深入探讨为什么将数据库放入 K8S 中不是一个明智的选择。
Kubernetes (k8s)是一个非常优秀的容器编排工具,它的目标是帮助开发者更好地管理海量复杂的无状态应用服务。尽管它提供了诸如 StatefulSet、PV、PVC、LocalhostPV 等抽象原语用于支持有状态服务(i.e. 数据库),但这些东西对于运行有着更高可靠性要求的生产级数据库服务来说仍然远远不够。
数据库是“宠物”而非“家畜”,需要细心地照料呵护。将数据库放入K8S作为“牲畜”对待,本质上是将外部的磁盘/文件系统/存储服务变为了新的“数据库宠物”。使用 EBS/网络存储/云盘运行数据库,在可靠性与性能上有巨大劣势;然而如果使用高性能本地NVMe磁盘,与节点绑定无法调度的数据库又失去了放入K8S的主要意义。
将数据库放入 K8S 中会导致“双输” —— K8S 失去了无状态的简单性,不能像纯无状态使用方式那样灵活搬迁调度销毁重建;而数据库也牺牲了一系列重要的属性:可靠性,安全性,性能,以及复杂度成本,却只能换来有限的“弹性”与资源利用率 —— 但虚拟机也可以做到这些!对于公有云厂商之外的用户来说,几乎都是弊远大于利的。
以 K8S为代表的“云原生”狂热已经成为了一种畸形的现象:为了k8s而上k8s。工程师想提高不可替代性堆砌额外复杂度,管理者怕踩空被业界淘汰互相卷着上线。骑自行车就能搞定的事情非要开坦克来刷经验值/证明自己,却不考虑要解决的问题是否真的需要这些屠龙术 —— 这种架构杂耍行为终将招致恶果。
我们认为在分布式网络存储的可靠性与性能超过本地存储前,将数据库放入 K8S 是一种不明智的选择。解决数据库管理复杂度并非只有 K8S 一条道路,开箱即用的开源RDS —— Pigsty 基于裸操作系统提供了另一种选择。用户应当擦亮双眼,根据自己的真实情况与需求做出明智的利弊权衡与技术决策。
K8S 在无状态应用服务编排领域内表现出色,但一开始对于有状态的服务极其有限 —— 尽管运行数据库并不是 K8S 与 Docker 的本意,然而这阻挡不了社区对于扩张领地的狂热 —— 布道师们将 K8S 描绘为下一代云操作系统,断言数据库必将成为 Kubernetes 中的普通应用一员。而各种用于支持有状态服务的抽象也开始涌现:StatefulSet、PV、PVC、LocalhostPV。
有无数云原生狂热者开始尝试将现有数据库搬入 K8S 中,各种数据库的 CRD 与 Operator 开始出现 —— 仅以 PostgreSQL 为例,在市面上就已经可以找到至少十款以上种不同的 K8S 部署方案:PGO,StackGres,PostgresOperator,CloudNativePG,TemboOperator,KubeDB,PerconaOperator,Kubegres,KubeDB,KubeBlocks,……,琳琅满目。CNCF 的景观图就这样开始迅速扩张,成为了复杂度乐园。
然而复杂度也是一种成本,随着“降本增效”成为主旋律,反思的声音开始出现 —— 下云先锋 DHH 在公有云上深度使用了 K8S,但在回归开源自建的过程中也因为过分复杂而放弃了它,仅仅用 Docker 与一个名为 Kamal 的Ruby小工具作为替代。许多人开始思考,像数据库这样的有状态服务到底是不是应该放入 Kuberentes 中?
K8S为了支持有状态应用,变得越来越复杂,远离了容器编排平台的初心。以至于 Kubernetes 的联合创始人Tim Hockin 也在今年的 KubeCon 上罕见地发了声:《K8s在被反噬!》:“*Kubernetes 变得太复杂了,它需要学会克制,否则就会停止创新,直至丢失自己的基本盘* ” 。
对于有状态的服务,云原生领域非常喜欢用一个“宠物”与“牲畜”的类比 —— 前者需要精心照料,细心呵护,例如数据库;而后者可以随意处置,一次性用完即丢,就是普通的无状态应用(Disposability)。
云原生应用12要素:Disposability
K8S的一个主要架构目标就是,把能当畜生的都当畜生处理。对数据库进行 “存算分离”就是这样一种尝试:把有状态的数据库服务拆分为K8S外的状态存储与K8S内的纯计算部分,状态放在云盘/EBS/分布式存储上,而“无状态”的数据库进程就可以塞进K8S里随意创建销毁与调度了。
不幸的是,数据库,特别是 OLTP 数据库是重度依赖磁盘硬件的,而网络存储的可靠性与性能相比本地磁盘仍然有数量级上的差距。因而 K8S 也提供了LocalhostPV 的选项 —— 允许用户在容器上打一个洞,直接使用节点操作系统上的数据卷,直接使用高性能/高可靠性的本地 NVMe 磁盘存储。
但这让用户面临着一个抉择:是使用垃圾云盘并忍受糟糕数据库的可靠性/性能,换取K8S的调度编排统一管理能力?还是使用高性能本地盘,但与宿主节点绑死,基本丧失所有灵活调度能力?前者是把压舱石硬塞进 K8S 的小船里,拖慢了整体的灵活性与速度;后者则是用几根钉子,把K8S小船锚死在某处。
运行单独的纯无状态的K8S集群是非常简单可靠的,运行在物理机裸操作系统上的有状态数据库也是十分可靠的。然而将两者混在一起的结果就是双输:K8S失去了无状态的灵活与随意调度的能力,而数据库牺牲了一堆核心属性换来了很多对数据库根本不重要的“弹性”与Day1交付速度。
关于前者,一个鲜活的案例是由 KubeBlocks 贡献的 PostgreSQL@K8s 性能优化记。k8s 大师上了各种高级手段,解决了裸金属/裸OS上根本不存在的性能问题。关于后者的鲜活的案例是滴滴的K8S架构杂耍大翻车,如果不是将有状态的 MySQL 放在K8S里,单纯重建无状态 K8S 集群并重新发布应用,怎么会要12小时这么久才恢复?
对于严肃的生产技术选型决策,最重要的永远是利弊权衡。这里我们按照常用的“质量、安全、效率、成本”顺序,来聊一下K8S放数据库相对于经典裸金属/VM部署在技术上的利弊权衡。我并不想在这里写一篇面面俱到,好像什么都说了的论文,而是抛出一些具体问题,供大家思考与讨论。
在质量上:K8S相比物理部署新增了额外的失效点与架构复杂度,拉高了爆炸半径,并且会显著拉长故障的平均恢复时长。在《数据库放入Docker是个好主意吗?》一文中,我们已经给出了关于可靠性的论证,同样的结论也可以适用于 Kubernetes —— K8S 与 Docker 会为数据库引入额外且不必要的依赖与失效点,而且缺乏社区故障知识积累与可靠性战绩证明(MTTR/MTBF)。
在云厂商的分类体系中,K8S属于PaaS,而RDS属于更底层的IaaS。数据库服务比K8S有着更高的可靠性要求:例如,许多公司的云管平台都会依赖一个额外的 CMDB 数据库。那么这个数据库应该放在哪里呢?你不应该把 K8S 依赖的东西交给 K8S 自己来管理,也不应该添加没有必要的额外依赖,阿里云全球史诗大故障 与 滴滴K8S架构杂耍大翻车 为我们普及了这个常识。而且,如果已经有了K8S外的数据库,再去维护一套K8S内的数据库体系就更得不偿失了。
在安全上:多租户环境中的数据库新增了额外的攻击面,带来了更高的风险与更复杂的审计合规挑战。K8S 会让你的数据库更安全吗?也许K8S架构杂耍的复杂度景象会劝退不熟悉K8S的脚本小子,但对真正的攻击者而言,更多的组件与依赖往往意味着更广的攻击面。
在《阿里云PostgreSQL 漏洞技术细节》中,安全人员利用一个自己的 PostgreSQL 容器逃脱到K8S主机节点中,并可以访问 K8S API 与其他租户的容器与数据。而这很明显是 K8S 专有的问题 —— 风险是真实存在的,这样的攻击已经发生,并让本土云厂领导者阿里云中招翻车。
《The Attacker Perspective - Insights From Hacking Alibaba Cloud[1]》
在效率上:如《数据库放入Docker是个好主意吗?》一文所述,不论是额外的网络开销,Ingress 瓶颈,拉垮的云盘,对于数据库的性能都会产生负面影响。又比如《PostgreSQL@K8s 性能优化记》 所揭示的 —— 你需要相当程度的技术水平功力,才能让 K8S 中的数据库性能堪堪(并没有)持平于裸机。
Latency 的单位是 ms 不是 µs,差点以为眼花了。
在成本上,K8S与各种Operator提供了一个不错的抽象,封装了一部分数据库管理的复杂度,对于没有DBA的团队有一定的吸引力。然而使用它管理数据库所减少的复杂度,比起使用K8S本身引入的复杂度来说就相形见绌了。比如,随机发生的IP地址漂移与Pod自动重启,对于无状态应用来说可能并不是一个大问题,然而对数据库来说这就令人难以忍受了 —— 许多公司不得不尝试魔改 kubelet 以规避这一行为,进而又引入更多的复杂度与维护成本。
正如《从降本增笑到降本增效》“降低复杂度成本” 一节所述:智力功率很难在空间上累加:当数据库出现问题时需要数据库专家来解决;当 Kubernetes 出现问题时需要 K8S 专家看问题;然而当你把数据库放入 Kubernetes 时,复杂度出现排列组合,状态空间开始爆炸,然而单独的数据库专家和 K8S 专家的智力带宽是很难叠加的 —— 你需要一个双料专家才能解决问题,而这样的专家比起单纯的数据库专家无疑要少得多也贵得多。这样的架构杂耍足以让包括头部公有云/大厂在内的绝大多数团队,在遇到故障时出现大翻车。
一个有趣的问题是,既然 K8S 并不适用于有状态的数据库,那么为什么还有这么多厂商 —— 包括 “大厂” 在争先恐后地做这件事呢?恐怕这里的原因并不是技术上的。
Google 照着内部的 Borg 宇宙飞船做了艘 K8S 战舰开源出来,老板们怕踩空被业界淘汰进而互相卷着上线,觉得自己用上 K8S 就跟Google一样牛逼了 —— 有趣的是Google自己不用,开源出来搅屎AWS忽悠业界;绝大多数公司并没有 Google 那样的人手去操作战舰,更重要的是他们的业务可能只要一艘舢舨就解决了。裸机上的 MySQL + PHP , PostgreSQL+ Ruby / Python / Go ,已经让无数公司一路干到上市了。
在现代硬件条件下,绝大多数应用,终其生命周期的复杂度都不足以用到 K8S 来解决。然而,以 K8S为代表的“云原生”狂热已经成为了一种畸形的现象:为了k8s而上k8s。一些工程师的目的是去寻找足够“先进”足够酷的,最好是大公司在用的东西来满足自己跳槽,晋升等个人价值的需求,或者趁机堆砌复杂度以提高自己的 Job Security,而压根不是考虑要解决问题是否真的需要这些屠龙术。
,时长00:06
云原生领域被各种花里胡哨的项目充满,整得跟菜市场一样:每个新来的开发团队都想引入一套新东西,今天一个 helm 明天一个 kubevela,说起来都是光明前途,效率拉满,实际上成为了 YAML Boy 的架构屎山与复杂度乐园。折腾最新的技术,发明大把的概念,经验值和声望是自己的,复杂度代价反正是用户买单,搞出问题还可以再敲一笔维护费,简直完美!
CNCF Landscape
云原生运动的理念是很有感召力的 —— 让本来是公有云专属的弹性调度能力普及到每一个用户身上。K8S 也确实在无状态应用上表现出色。然而过度的狂热已经让 K8S 偏离了原本的初心与方向 —— 简单地做好无状态应用编排调度这件事,被支持有状态应用的妄念拖累的也不再简单了。
刚接触K8S时,我也曾有过这种皈依者狂热 —— 几年前在探探我们也有着两万多核几百套数据库,我迫切地想要尝试将数据库放入 Kubernetes 中,并测遍了各种 Operator。然而在前后长达两三年的方案调研与架构设计中,我最终冷静下来,并放弃了这种疯狂的打算 —— 而是选择基于裸金属/裸操作系统架构我们自己的数据库服务。因为对我们来说,K8S为数据库带来的收益相比其引入的问题与麻烦,实在是微不足道。
数据库应该放入K8S里吗?这取决于具体场景:对于从资源利用率里用超卖刨食吃的云厂商而言,弹性与资源利用率非常重要,它们直接与收入和利润挂钩;稳定可靠效率都得屈居其次 —— 毕竟可用性低于3个9也不过按SLA赔偿本月消费 25%的代金券而已。但是对于我们自己,以及生态光谱中的大多数用户而言,这些利弊权衡就不成立了:一次性的 Day1 Setup效率,弹性与资源利用率并不是他们最关心的问题;可靠性、性能、Day2 Operation成本,这些数据库的核心属性才是最重要的。
我们将自己的数据库服务架构方案开源出来 —— 即开箱即用的 PostgreSQL 发行版与本地优先的 RDS 替代: Pigsty。我们没有选择 K8S 与 Docker 这种所谓“一次构建,到处运行” 的讨巧办法,而是一个一个地去适配不同的操作系统发行版/不同的大版本,并使用 Ansible 实现类 K8S CRD IaC 的效果封装管理复杂度。这确实是一件非常幸苦的工作,但却是正确的事情 —— 这个世界并不需要又一个在 K8S 中放入PG数据库玩具积木的拙劣尝试,但确实需要一个最大化发挥出硬件性能与可靠性的生产数据库服务架构方案。
Pigsty vs StackGres
也许有一天,当分布式网络存储的可靠性与性能可以超过本地存储的表现时,以及当主流数据库都对存算分离有一定程度上的支持后,事情会再次发生变化 —— K8S 变得适用于数据库起来。但至少就目前来讲,我认为将严肃的生产 OLTP 数据库放入 K8S ,仍然是不成熟与不合时宜的。希望读者可以擦亮双眼,在这件事上做出明智的选择。
Docker 的诅咒:曾以为它是终极解法,最后却是“罪大恶极”?
重新拿回计算机硬件的红利** **
Running Database on Kubernetes
[1] The Attacker Perspective - Insights From Hacking Alibaba Cloud: https://www.youtube.com/watch?v=d81qnGKv4EE
[2] 按SLA赔偿本月消费: https://vonng.com/cn/blog/cloud/sla/
[3] Pigsty: https://vonng.com/cn/blog/db/pigsty-intro/
[4] 《Running Database on Kubernetes》: https://questdb.io/blog/databases-on-k8s/