Transcript Go 语言高并发实战
Go 语言高并发实战
构建千万级在线的实时消息推送服务
2012.12.23
关于我
•
•
•
•
•
张景埕,网名 diogin(/’daɪədrɪn/)
programmer@360
Go 语言追随者与实践者
新浪微博:http://weibo.com/diogin
[email protected]
摘要
•
•
•
•
•
一、为什么选用 Go
二、探究 Go 语言的实现细节
三、实时消息推送服务的特点
四、系统架构及组件细节
五、数据、经验和教训
一、为什么选用 Go
1. 高并发
• 一个 Go 进程可以轻易支撑几十万上百万并
发运行的 Go 例程(只要你内存足够大)
• O(1) 的调度
• 5KiB/goroutine 的内存开销
• net 包:pollServer (epoll/kqueue/iocp) 支
持大量并发连接 fd 的事件通知,同时还支
持多核并行 (目前为 8 核)
• select/channel 提供卓越的例程间通信能力
2. 高性能
•
•
•
•
编译为本地机器码
静态链接 (CGO_ENABLED=0)
轻量级的 runtime 实现
热点代码可以直接编写 C 或 ASM,同样静
态链接进目标文件
3. 垃圾收集
•
•
•
•
不想为内存泄露问题焦头烂额
朴素的实现:停顿式、标记 & 清除
还有很大的提升空间
资源泄露还得自己解决
4. 低成本
• 学习成本低:源自 C 系,大量的 C 系程序
员可以很快上手(这很重要)
• 运维成本低:不需要安装各种让人头疼的
依赖,不需要搭建各种运行环境,部署非
常方便
5. 够用的标准库
•
•
•
•
•
•
•
•
net
encoding/gob
strings, bytes, errors, strconv
regexp
os, time
syscall
sync
......
6. 高移植性
• 多种操作系统:Linux, FreeBSD, Darwin,
NetBSD, OpenBSD, Windows
• 多种体系结构:386, amd64, arm(v5,v6,v7)
• 多种目标文件格式:elf/pe/macho
7. 优秀的背景
• Google, BSD License
• Ken Thompson, Rob Pike, Robert
Griesemer, Russ Cox, Brad Fitzpatrick...
• 拥有强烈的 Plan 9 血统
• C, Python 的风格
Alternatives?
•
•
•
•
Erlang
Node.js
D
Rust
二、探究 Go 语言的实现细节
WHY?
•
•
•
•
•
•
产品级的应用,不是玩具
在国内(甚至国外),仍处于吃螃蟹阶段
我们需要能 hold 住可能出现的各种问题
提升团队成员技术水平
回击各种针对 Go 的恶意 FUD 和各种 PK
回馈 Go 本身,积极为其做贡献
摘要
•
•
•
•
•
•
代码结构
逻辑结构
实现结构
程序启动
运行视角
深入 runtime
1. 代码结构
•
•
•
•
•
•
工具 + 标准库
include/:Go 基本工具依赖的库的头文件
src/:Go 基本工具、标准库的实现
src/cmd/:Go 基本工具及其实现
src/lib*/:Go 基本工具依赖的库的实现
src/pkg/:Go 标准库的实现
2. 逻辑结构
• gc (Go 编译器)、cc (Plan 9 C 编译器)、as
(Plan 9 汇编器)、ld (Plan 9 链接器)、pack
(Plan 9 目标文件归档工具) 工具链一条龙
• 支持多种操作系统、体系结构和可执行文
件格式
• 程序 = N 个包的合并、组合
• 包 = const、var、type、func
• dist:引导程序,负责构建 Go 基本工具
• go:管理 Go 的各项功能
3. 实现结构
• 每个包里可以有 .go、.c、.s 文件,分别由
gc、cc、as 编译,最后统一用 ld/pack 链
接并打包成静态链接库(遵循一致的 ABI)
• 每个 Go 程序都包含最底层的 runtime 包,
runtime 实现并封装了程序的运行时环境
• syscall 包封装了操作系统调用,同时与
runtime 协作进行 goroutine 调度
• sync 提供了基本的并发同步原语(工具包)
• reflect 为应用层代码提供运行时自省能力
4. 程序启动
•
•
•
•
基本概念:M—内核线程,G—goroutine
Go 使用符号“·”分隔包与包内的成员名字
第一个 M 叫 m0,每个 M 的调度例程叫 g0
准备 runtime·m0/runtime·g0 -> 准备 argc,
argv -> 调度器初始化(内存初始化 -> m 初
始化 -> 注册 args, envs -> 设置并行参数)
-> 创建并运行 runtime·main 例程(启动垃圾
收集器 -> main·init() -> main·main())
5. 运行视角
• 程序由多个包构成,入口为 main 包,每个
包在程序启动时按依赖顺序逐一初始化
• 程序内有多个 goroutine 并发运行,
goroutine 通过 channel 进行同步/异步通信
• 自动化内存管理,不区分堆和栈
6. 深入 runtime
• mgc0.c:垃圾收集器
• proc.c:goroutine 管
理及调度
• hashmap.c:Go map
• chan.c:channel 实现
• malloc.goc:内存分配
器
• asm_GOARCH.s:体
系结构相关功能
• iface.c:Go interface
• symtab.c:符号表
• cpuprof.c/mprof.goc:
Profiling
• sys_GOOS.s:操作系
统相关功能
• thread_GOOS:内核
线程统一抽象
• panic:panic管理
三、实时消息推送服务的特点
摘要
•
•
•
•
•
•
•
•
长连接(多种接入协议:HTTP、TCP)
高并发(>= 10,000,000)
多种发送方式(支持单播、多播、广播)
持久/非持久
准实时(200ms ~ 2s)
客户端多样性(手机端、PC 端)
同一账号多客户端同时接入
接入网络频繁变化(电信、联通……)
1. 长连接
•
•
•
•
Server PUSH 的基础
HTTP Long Polling(Keep-Alive)
基于 TCP 的自定义通信协议
采用心跳来侦测对方是否还在线
2. 高并发
• C10K? out 了,现在的标准是 C1000K
• 用户众多:各接入产品实时在线人数都在
10,000,000 以上
• Linux Kernel >= 2.6.32, x86-64
• sysctl -w fs.file-max=12000000
• sysctl -w fs.nr_open=11000000
• limit.conf: nofile=10000000
• TCP/IP 协议栈参数调优
3. 多种发送方式
• 点对点聊天(单播)
• 定点推送(多播)
• 全网推送(广播)
4. 持久/非持久
• 持久消息必须保证不丢失 (需要离线存储)
• 非持久消息仅发给当前在线用户
5. 准实时
• 发一条消息,另一个人必须能很快收到
• 200ms ~ 2s
• GC 卡顿会造成巨大麻烦
6. 客户端多样性
• 手机、PC、平板电脑
• 不同的产品有不同的需求
• 提供机制,而非策略
7. 同一账号多客户端接入
•
•
•
•
iMessage:iPhone, iPad, MacOSX
QQ:PC、手机、平板、微信
互斥(早期 QQ)& 共存
策略:如何控制?
8. 接入网络频繁变化
• 白天在电信,晚上在联通
• 一会儿在公司 wifi,一会儿 3G,一会儿 2G
• 断线,重连,断线,重连……
四、系统架构及实现细节
摘要
•
•
•
•
•
•
•
逻辑架构:简单至上
组件:room
组件:register
组件:saver
组件:idgenerator
组件:center
存储:redis
1. 逻辑架构:简单至上
2. 组件:room
•
•
•
•
•
客户端所连接的进程,类似于一个聊天室
每个客户端一个 server goroutine 进行下推
每个 server 有一个 channel 存消息队列
room 内有 book 记录 user 与 server 映射
一个 http server 负责收消息并将消息路由
到接收人所在的 room 和 server
• manager 负责掌控 room 的服务:内部单
播、多播、广播
• admin 负责 room 进程的管理
3. 组件:register
• 由于 room 的分布式与全对称设计,需要有
一个地方记录用户当前连到了哪个 room,
register 实现该角色
• 同时需要记录在线时长等信息 (业务需求)
• 本质上就是一个 key -> value 的 map
• value 是个 struct
• hash 算法定位 register 进程
• 可以直接用 redis,但自己实现可以方便地
添加业务逻辑
4. 组件:saver
• 分布式全对称设计
• 提供存储接口,封装后端的分布式存储
• 接口采用 encoding/gob 编码格式的 rpc
5. 组件:idgenerator
•
•
•
•
全局消息 id 生成器,int64
分布式,每个进程负责一块 id 区域
保证不重叠
后台 goroutine 每隔一秒写一次磁盘,记录
当前 id
• 启动时跳过一段 id,防止一秒内未写入磁
盘的 id 重复生成
6. 组件:center
• 提供消息操纵接口给应用服务器调用
• 运营人员后台发消息 -> 应用服务器 ->
center -> room -> 客户端
• RESTful API
• 有些任务(比如广播)需要一段时间,运
营人员需要追踪发送进度,可能要临时停
止,因此需要有“任务”概念,并可管理
• 提供统计接口
7. 存储:redis
•
•
•
•
核心数据
db_users:ZSET,存各产品的用户集合
db_slots:LIST,存用户离线消息队列
db_buckets:DICT,存消息 id -> 消息体
五、数据、经验和教训
数据
• 16台机器,标配24个硬件线程,64GB内存
• Linux Kernel 2.6.32 x86_64
• 单机80万并发连接,load 0.2~0.4,CPU
总使用率 7%~10%,内存占用20GB (res)
• 目前接入的产品约1280万在线用户
• 2分钟一次GC,停顿2秒 (1.0.3 的 GC 不给
力,直接升级到 tip,再次吃螃蟹)
• 15亿个心跳包/天,占大多数
• 持续运行一个月无异常,稳定
经验
•
•
•
•
•
敢于尝试新东西,勇于面对新挑战
拿代码说话,拿数据说话
不要凭感觉,要实际去测
对一项新技术要切实了解其优势和劣势
对于感觉模糊的地方,追查其实现,做到
有备无患
教训
• 实时系统对 GC 非常敏感,幸好 tip 上提供
了并行 GC,否则这就是一次惨痛教训,需
要做架构方面的大调整 (将大进程拆分成小
进程)
Thanks
Q&A
张景埕 [email protected]
http://weibo.com/diogin