ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Docker Compose部署TiDB集群实战:从组件架构到踩坑排查

Docker Compose部署TiDB集群实战:从组件架构到踩坑排查

前两天刚在本地用Docker把一套TiDB集群编排起来,从拉镜像到最终用MySQL客户端连上4000端口,整个过程比我预想中多踩了不少坑。现在把这套流程整理出来,顺带把那些官方文档里写得云淡风轻、实际却能卡你半小时的细节都标记清楚,给正在折腾docker部署TiDB的朋友做个参考。

TiDB是一个兼容MySQL协议的分布式NewSQL数据库,简单说就是“看着像MySQL,但底层是分布式存储,能横向扩展”。它拆成了TiDB Server、PD、TiKV三个核心组件,Docker部署的核心思路就是把这三个组件分别容器化、用Compose编排起来,让单机也能快速体验分布式数据库的架构。这篇文章适合两类人看:一是刚接触TiDB、想用最低成本跑起来玩玩的开发者,二是被各种“用K8s部署”“二进制部署”文档劝退、只想要一套能快速复现的本地演示环境的人。我尽量少讲大道理,多写可以直接抄的配置和真实踩坑过程。

1. 先搞清楚TiDB的三兄弟再动手

1.1 三个核心组件各自干什么

TiDB不是一个单体数据库,而是一组配合工作的分布式组件。我习惯用一家餐厅来类比:TiDB Server是前台点餐的收银员,负责接收SQL请求、返回结果;PD是店长,负责掌握全局信息、决定哪桌客人去哪个窗口取餐;TiKV是后厨,真正把饭做熟、把数据存下来。

具体到技术担当:

  • TiDB Server:无状态SQL层,负责解析SQL、生成执行计划、跨节点聚合数据。因为无状态,所以可以随便扩缩容,前面挂个负载均衡就能当MySQL用。
  • PD(Placement Driver):整个集群的“大脑”,负责元数据管理、分片(Region)调度、时间戳分配(TSO)。PD通常部署3个节点,通过Raft协议保证一致性。
  • TiKV:有状态的存储层,数据以Region为单位打散在所有TiKV节点上,每个Region默认3副本,副本之间通过Raft协议自动同步。这是TiDB能支持弹性和高可用的根基。

除了这三兄弟,还有一个TiFlash,属于列存引擎,用于加速分析类查询。名字叫“TiFlash”,实际上就是官方把列存和行存分开存储的HTAP方案。本地演示可以不部署,后面进阶再玩。

1.2 为什么用Docker而不是裸机装二进制

我第一次折腾TiDB时也想过直接下载二进制包跑,后来发现一个很现实的问题:一套最少规模的集群至少是3个PD加3个TiKV加1个TiDB,也就是7个进程。手动管理这么多进程,启动顺序错了就会导致成员注册不上,日志还分散在各个目录。用Docker的收益是明确的:

  • 依赖打包干净,TiDB组件没有乱七八糟的宿主机依赖,一条命令就能起一个组件,形象理解就是“扔进集装箱就跑”。
  • 网络模型统一,Compose会自动创建内部网络,组件之间用服务名互访,不需要手动改hosts。
  • 环境隔离,组件之间互不影响,内存限制、日志采集、数据卷挂载都比裸进程好管理。

我个人的观点是:如果只是想快速体验TiDB、验证应用兼容性、跑个Demo,Docker Compose是性价比最高的方式。但如果目标是学习TiDB运维、部署到生产,那还是得看K8s,因为Docker Compose不提供故障自愈和自动扩缩容。

1.3 部署形态怎么选

围绕“docker部署TiDB”,实际有三种常见的玩法,应用场景完全不一样:

玩法适用场景优点缺点
单个docker run起容器临时测试TiDB,不关心其他组件最快,一条命令组件间要手动指定网络IP,容易乱
Docker Compose编排整套集群本机/单机演示环境配置清晰,启停方便不适合跨多机部署
K8s+Operator生产环境、弹性扩缩容自愈、自动运维能力强学习成本高,资源开销大

这篇主要讲第二种,它也是单机环境下最贴近“真实集群拓扑”的姿势。多机场景下,Compose需要额外配置外部网络,复杂度成倍上升,不如直接用K8s。

2. 部署前准备与配置文件设计

2.1 环境准备

在写配置之前,先把基础环境确认好。我用的是一台8核16G内存的Linux服务器(Ubuntu),Docker和Docker Compose插件都已经装好。实际跑起来后,全套TiDB集群大概占用6G左右的空闲内存,所以建议至少8G内存,否则很容易出现OOM。

环境要求汇总如下:

  • Docker Engine 20.10以上,支持Compose v2
  • 可用磁盘空间至少15G,镜像加数据卷很占空间,TiKV镜像本身就超过1G
  • 服务器时间准确,最好已配置NTP时间同步。TiDB对时钟偏移极为敏感,PD和TiKV之间时钟偏差太大会直接拒绝工作
  • 端口可自行调整,但默认规划要心里有数

注意:TiDB对时间同步的要求源自分布式一致性原理。Raft协议本身依赖于消息的先后顺序,如果两个节点之间的时钟偏差过大,会引发TSO时间戳乱序,轻则报错,重则选主异常。这个问题我后面细讲。

2.2 端口规划

要搞清楚每个组件暴露哪些端口,不然端口冲突排查时毫无头绪。以一套最常规的部署为例:

组件端口用途
TiDB Server4000SQL服务端口,MySQL协议入口
TiDB Server10080TiDB状态上报端口(HTTP)
PD2379PD客户端端口,TiKV/TiDB都连它
PD2380PD集群节点间通信端口(peer)
PD9090? 不,是部分版本会用到,具体按实际镜像
TiKV20160TiKV客户端端口
TiKV20180TiKV状态上报端口(HTTP)

这里有个容易混淆的点:PD的2379端口是让TiDB和TiKV来连接的,2380端口是PD自己节点之间通信用的。如果你在Compose里没把2380暴露出来,只是容器内部使用,那其实也是可以的,但为了能从宿主机查看健康状态,我通常会暴露出来。

2.3 docker-compose.yml怎么写、为什么这么写

这是一个非常典型的Compose配置,完整版我放在下面。先说几个关键判断:镜像用官方镜像仓库的版本tag,不要用latest裸跑,因为大版本升级可能导致配置不兼容;TiKV和PD都做了数据卷挂载,防止容器删除后数据丢失;启动顺序通过depends_on加healthcheck控制,这一步几乎是必须的,否则TiKV一启动就连接PD,PD还没就绪,经常出现注册失败。

services: pd1: image: pingcap/pd:v8.1.0 container_name: pd1 ports: - "2379:2379" - "2380:2380" command: - --name=pd1 - --client-urls=http://0.0.0.0:2379 - --peer-urls=http://0.0.0.0:2380 - --advertise-client-urls=http://pd1:2379 - --advertise-peer-urls=http://pd1:2380 - --initial-cluster=pd1=http://pd1:2380,pd2=http://pd2:2380,pd3=http://pd3:2380 volumes: - ./data/pd1:/data healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:2379/pd/api/v1/health || exit 1"] interval: 3s timeout: 3s retries: 20 pd2: image: pingcap/pd:v8.1.0 container_name: pd2 ports: - "12379:2379" - "12380:2380" command: - --name=pd2 - --client-urls=http://0.0.0.0:2379 - --peer-urls=http://0.0.0.0:2380 - --advertise-client-urls=http://pd2:2379 - --advertise-peer-urls=http://pd2:2380 - --initial-cluster=pd1=http://pd1:2380,pd2=http://pd2:2380,pd3=http://pd3:2380 volumes: - ./data/pd2:/data healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:2379/pd/api/v1/health || exit 1"] interval: 3s timeout: 3s retries: 20 pd3: image: pingcap/pd:v8.1.0 container_name: pd3 ports: - "22379:2379" - "22380:2380" command: - --name=pd3 - --client-urls=http://0.0.0.0:2379 - --peer-urls=http://0.0.0.0:2380 - --advertise-client-urls=http://pd3:2379 - --advertise-peer-urls=http://pd3:2380 - --initial-cluster=pd1=http://pd1:2380,pd2=http://pd2:2380,pd3=http://pd3:2380 volumes: - ./data/pd3:/data healthcheck: test: ["CMD-SHELL", "curl -f http://localhost:2379/pd/api/v1/health || exit 1"] interval: 3s timeout: 3s retries: 20 tikv1: image: pingcap/tikv:v8.1.0 container_name: tikv1 ports: - "20160:20160" - "20180:20180" command: - --addr=0.0.0.0:20160 - --advertise-addr=tikv1:20160 - --status-addr=0.0.0.0:20180 - --pd-endpoints=pd1:2379,pd2:2379,pd3:2379 - --data-dir=/data volumes: - ./data/tikv1:/data depends_on: pd1: condition: service_healthy pd2: condition: service_healthy pd3: condition: service_healthy tikv2: image: pingcap/tikv:v8.1.0 container_name: tikv2 ports: - "30160:20160" - "30180:20180" command: - --addr=0.0.0.0:20160 - --advertise-addr=tikv2:20160 - --status-addr=0.0.0.0:20180 - --pd-endpoints=pd1:2379,pd2:2379,pd3:2379 - --data-dir=/data volumes: - ./data/tikv2:/data depends_on: pd1: condition: service_healthy pd2: condition: service_healthy pd3: condition: service_healthy tikv3: image: pingcap/tikv:v8.1.0 container_name: tikv3 ports: - "40160:20160" - "40180:20180" command: - --addr=0.0.0.0:20160 - --advertise-addr=tikv3:20160 - --status-addr=0.0.0.0:20180 - --pd-endpoints=pd1:2379,pd2:2379,pd3:2379 - --data-dir=/data volumes: - ./data/tikv3:/data depends_on: pd1: condition: service_healthy pd2: condition: service_healthy pd3: condition: service_healthy tidb: image: pingcap/tidb:v8.1.0 container_name: tidb ports: - "4000:4000" - "10080:10080" command: - --store=tikv - --path=pd1:2379,pd2:2379,pd3:2379 - --advertise-address=tidb depends_on: tikv1: condition: service_started tikv2: condition: service_started tikv3: condition: service_started

这个配置有几个设计上的讲究,我展开说说。

第一,PD为什么要做healthcheck。TiKV启动的时候会去PD注册自己的Store信息,如果PD还没就绪,TiKV会一直重试。但TiKV的重试机制并不会在PD恢复后自动把所有事情都处理得很好,某些极端情况下会出现Store状态为Offline或Tombstone。给PD加一个HTTP健康检查,TiKV严格等待PD就绪后再启动,是最简单可靠的顺序控制方式。

第二,advertise地址为什么不能用127.0.0.1。容器内部和宿主机是两个网络命名空间。如果TiKV的advertise-addr写成了127.0.0.1:20160,PD拿到的Store地址就是错的,客户端访问时就会连接失败。在Compose网络里,容器之间通过服务名互相访问,所以advertise地址必须是服务名。这就是为什么我不建议用docker run + --network host来部署这套集群,省事是省事,但各个进程都抢宿主机端口,后期管理很混乱。

第三,数据卷要挂到宿主机。TiDB组件的数据目录各不相同,PD默认存元数据到/data,TiKV的--data-dir也指向/data。不挂卷的话,docker-compose down之后数据就跟着容器一起没了。挂载之后,即使容器被删掉重建,数据也都还在。

3. 从启动到连库:完整实操记录

3.1 启动集群

配置文件保存为docker-compose.yml,然后在同一目录执行:

docker-compose up -d

首次启动会拉镜像,几个镜像加起来体积不小,耐心等就行。启动成功后,先用docker-compose ps看一下状态:

docker-compose ps

我那次启动后出现的情况是:三个PD都处于healthy状态,三个TiKV还在Restarting。这说明TiKV启动失败,正在被Compose反复拉起。这时候别干等着,赶紧看日志:

docker logs tikv1 --tail 50

这类问题后面在踩坑实录里专门说。正常情况下,等所有容器都显示Up之后,再用tiup或mysql客户端连上去验证。

3.2 用MySQL客户端验证

TiDB的4000端口完全兼容MySQL协议,所以直接用mysql客户端就能连:

mysql -h 127.0.0.1 -P 4000 -u root

TiDB默认没有密码,进去后先看版本:

select version(); select tidb_version()\G

再跑一个简单的建库建表测试:

create database if not exists test; use test; create table if not exists t1(id int primary key auto_increment, name varchar(20)); insert into t1(name) values('hello'),('tidb'),('docker'); select * from t1;

结果返回正常,说明TiDB Server已经能成功读写TiKV了。这里尤其要确认一点:SQL走到TiKV存储,而不是走到了什么别的临时引擎。你可以在TiDB里执行explain analyze select * from t1,如果执行计划中出现了tikv_xxx算子,说明数据真的写到TiKV了。

我习惯同时开两个窗口,一个跑SQL,一个用tidb的http状态接口看负载情况:

curl http://127.0.0.1:10080/status

会返回一个JSON,里面包含版本、当前连接数、语句数等。本地验证时,这个接口比任何监控面板都直观。

3.3 数据持久化:容器删了也不怕

Docker容器是脆弱的,docker-compose down或者docker-compose rm都会把容器删掉。如果你没挂载数据卷,TiKV里面存的数据会全部消失,PD里的元数据也没了,集群等于重新初始化。

我强烈建议在任何一次正经操作之前,都先做一次容器删除重建的验证:

docker-compose down docker-compose up -d

利用挂载到宿主机data目录的数据目录,PD启动时会从已有数据里恢复元数据,TiKV也能从已有数据里恢复Region信息。重建之后,之前的库表和数据依然还在。这个验证做完,你对这套环境的信心会完全不同。

不过要注意一点:不要直接用宿主机上的同一个数据目录同时跑两套Compose集群。PD和TiKV的数据文件之间没有集群标识隔离,你硬要用同一份数据同时启动两套集群,很可能把集群元数据搞坏。

3.4 关闭集群的正确姿势

关闭集群也有讲究。最安全的顺序是:先停TiDB,再停TiKV,最后停PD。因为TiDB会把正在执行的请求处理完或者优雅终止,TiKV停的时候会向PD发送关闭请求,把Region的心跳停下来,PD最后停,避免其他组件还依赖它的时候它先没了。

实际操作中,用docker-compose stop是最省心的,它会按依赖倒序发送SIGTERM信号。我一般不会直接docker-compose kill或者docker stop某个容器,因为那相当于强制断电,极端情况下可能导致Raft日志没刷完,恢复时数据校验要花很长时间。

4. 踩坑实录:部署TiDB最容易出的5个问题

4.1 PD起来了,TiKV却一直不可用

这是我第一次部署遇到的最大问题。现象是PD容器都healthy了,但tikv容器一直在Restarting,日志里反复出现连接PD超时或者member信息获取失败。

排查步骤:

  1. 确认PD地址在TiKV容器内可访问:docker exec tikv1 ping pd1,如果ping不通,八成是Compose网络问题,看看服务名是不是写错了。
  2. 确认advertise-addr配置正确。如果写成127.0.0.1,TiKV注册到PD的地址就是容器内部的回环地址,PD和其他节点访问不到。
  3. 查看PD日志,确认TiKV有没有成功发起RegisterStore请求。

最终我发现自己犯了一个低端错误——漏掉depends_on条件,TiKV在PD健康检查通过之前就开始启动了。后来加上condition: service_healthy,问题就消失了。

4.2 内存不够的OOM地狱

TiDB这套组件的资源占用不是说着玩的。3个PD、3个TiKV、1个TiDB,裸跑大概吃掉6~8G内存。如果服务器只有4G内存,TiKV启动后很快就会被内核OOM Kill掉,此时用docker-compose ps看,容器还显示Up,但日志里其实已经打印了Killed。

解决思路有两种。第一种是降低组件数量,把3个PD减到1个,TiKV也减到1个,这种配置在纯学习场景够用,但牺牲了高可用。第二种是给容器设置内存上限,在Compose里给每个服务加mem_limit:

deploy: resources: limits: memory: 2G

注意mem_limit对TiKV的效果一般,因为TiKV内部有RocksDB,默认block cache会尽量吃内存。想压内存,得给TiKV挂配置文件,限制block cache大小。开发环境可以用最小配置凑合,但生产环境千万别这么搞,性能会缩水很严重。

4.3 clock offset:分布式系统的隐形杀手

如果TiKV日志里出现clock offset相关错误,核心原因就是节点时间不同步。TiDB的PD会校验节点上送的时间戳,偏差过大会认为节点不可信。

本地单机部署也会遇到这个问题吗?会的,尤其是宿主机没配置NTP且使用了时间跳跃的情况下。我印象最深的一次是宿主机重启后,硬件时间漂移了十几秒,整个集群突然全部异常。

排查方法:

date

然后在三个PD容器里分别执行:

docker exec pd1 date docker exec pd2 date

正常应该是同一秒内。如果差了10秒以上,先同步宿主机时间:

timedatectl set-ntp true

如果容器时间和宿主机不一致,检查docker run时有没有挂载/etc/localtime。Compose里可以加:

volumes: - /etc/localtime:/etc/localtime:ro

这属于一个很容易被忽略但又非常关键的细节。

4.4 端口冲突排查

TiDB用4000端口,MySQL默认也是3306,一般不容易冲突。但如果宿主机本身装了别的应用,4000、2379、2380这些端口就很容易撞车。Compose启动时如果端口被占用,会直接报bind address already in use。

我的排查命令:

netstat -tlnp | grep -E '4000|2379|2380|20160'

遇到冲突,最简单的办法是把宿主机侧端口改掉,容器内部端口保持不变。比如前面配置里PD2、PD3的宿主机端口就是12379、22379,刻意避开了本机其他服务。容器内部的端口是组件之间的通信约定,不能随便改,但宿主机侧的端口只是映射入口,随便改不影响集群工作。

4.5 版本兼容和初始化连接问题

镜像版本选不好,同样会踩坑。比如PD和TiKV版本不一致,TiKV连上PD后,可能因为gRPC协议版本差异导致握手失败。所以必须统一版本号,不要出现pd用v8.0.0、tikv用v8.1.0这类混搭。如果看到日志里出现unknown descriptor或者protobuf parse error,优先检查各组件镜像版本是否一致。

另一个容易犯的错是,TiDB Server和TiKV都部署好了,但用客户端连的时候报错,比如host is not allowed to connect。这是TiDB的权限系统在捣乱。默认root用户只允许本机连接,如果你从别的机器连过来,需要手动创建用户并授权:

create user 'demo'@'%' identified by 'demo123'; grant all privileges on *.* to 'demo'@'%'; flush privileges;

如果不是必须用root,我建议所有外部连接都用这种方式创建独立账号,避免误操作把root密码改坏以后没法恢复。

5. 从演示环境到正经环境:下一步怎么玩

5.1 常用维护指令

这套Compose跑起来后,日常维护就这么几条命令,记住就行:

# 查看整体状态 docker-compose ps # 查看某个组件日志 docker-compose logs -f tikv1 # 进入TiDB容器执行SQL docker exec -it tidb mysql -h 127.0.0.1 -P 4000 -u root # 重启某个组件 docker-compose restart tidb # 停止所有容器但不删数据 docker-compose stop # 停止并删除所有容器(数据卷保留) docker-compose down

注意,docker-compose down不会删除挂载到宿主机目录的数据卷,所以数据安全。真正要清数据,只能手动删除宿主机data目录下的文件。

5.2 为什么生产环境不这么干

这是很多人会问的问题。既然Compose能跑,为什么生产环境不建议用?

核心原因有三个:一是Docker Compose是单机编排工具,没有跨节点调度能力,TiDB的分布式能力完全发挥不出来;二是TiDB的高可用依赖多副本和多节点,单机部署的Compose环境,即使起了3个TiKV,本质上还是共用一台物理机,硬件故障就全挂了;三是生产TiDB需要很多细粒度的参数调优,比如TiKV的线程池大小、RocksDB参数、PD的调度策略,Compose不适合承担这种级别的配置管理。

生产环境目前主流的部署方式是K8s + TiDB Operator,用K8s管理Pod、PV、网络、扩缩容,Operator负责执行TiDB特有的运维动作。学习路线可以是从Compose起步,理解组件交互,再过渡到K8s。

5.3 后续学习路径建议

跑通这套集群只是第一步,接下来我更推荐做这几件事:

第一,把故障演练做一遍。用docker-compose stop tikv1模拟单节点故障,观察TiDB是否还能正常读写。正常情况是:PD会在几秒内感知到Region副本缺失,把其他节点的副本提升为主副本,业务侧只是轻微卡顿。这个实验能让你彻底理解Raft多副本的价值。

第二,玩一下TiFlash。在Compose文件里追加一个tiflash服务,TiDB会自动把分析类查询路由到列存引擎。你可以在同一张表上跑普通OLTP查询和OLAP聚合,观察执行计划里的算子在行存和列存之间如何选择。

第三,试着把Prometheus和Grafana加进来。TiDB的Prometheus指标接口非常丰富,官方Grafana Dashboard模板也是现成的。本地把监控搭起来之后,你能直观看到Region数、SQL QPS、TiKV的CPU内存变化,排查问题会高效很多。

我个人在实际操作中的一个体会是:Docker部署TiDB这件事,门槛不在命令本身,而在你对分布式系统基本概念的把握。很多报错反推回去,都是对“PD是核心协调者”“TiKV数据多副本”“Raft一致性”这些基础理解不到位。把Compose环境当成实验田,多折腾几次,比单纯看文档管用得多。下一步如果感兴趣,可以试着把TiKV扩容到5节点、10节点,观察Region的调度过程,那时你会对“分布式数据库为什么这么设计”有更直观的理解。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进