数据中心里的并发编程


Google 的数据中心

数据中心程序:特点

“A network of computing and storage resources that enable the delivery of shared applications and data.” (CISCO)

以数据 (存储) 为中心

  • 互联网索引与搜索
    • Google
  • 社交网络
    • Facebook/Twitter
  • 支撑各类互联网应用
    • 通信 (微信/QQ)、支付 (支付宝)、游戏/网盘/……

数据中心:关键问题

如何实现高可靠、低延迟的多副本分布式存储和计算系统?

  • 在服务海量地理分布请求的前提下,三者不可兼得:
    • 数据要保持一致 (Consistency)
    • 服务时刻保持可用 (Availability)
    • 容忍机器离线 (Partition tolerance)

数据中心程序上的单机程序

事件驱动 + 高并发:系统调用密集延迟不确定

  • 网络数据读写
  • 持久存储读写
  • 单机程序目标:尽可能多地服务并行的请求
    • QPS: 吞吐量
    • Tail latency: 一个请求慢了,其他请求不能慢

假设有数千/数万个请求同时到达服务器……

  • 线程能够实现并行处理
  • 但远多于处理器数量的线程导致性能问题
    • 切换开销
    • 维护开销

协程:操作系统 “不感知” 的上下文切换

和线程概念相同 (独立堆栈、共享内存)

  • 但 “一直执行”,直到 yield() 主动放弃处理器
    • 有编译器辅助,切换开销低
      • yield() 是函数调用,只需保存/恢复 “callee saved” 寄存器
      • 线程切换需要保存/恢复全部寄存器
    • 但等待 I/O 时,其他协程就不能运行了……
      • 失去了并行

// 只可能是 1122 或 2211
void T1() { send("1"); send("1"); yield(); }
void T2() { send("2"); send("2"); yield(); }

Go 和 Goroutine

Go: 小孩子才做选择,多处理器并行和轻量级并发我全都要!

Goroutine: 概念上是线程,实际是线程和协程的混合体

  • 每个 CPU 上有一个 Go Worker,自由调度 goroutines
  • 执行到 blocking API 时 (例如 sleep, read)
    • Go Worker 偷偷改成 non-blocking 的版本
      • 成功 → 立即继续执行
      • 失败 → 立即 yield 到另一个需要 CPU 的 goroutine
        • 太巧妙了!CPU 和操作系统全部用到 100%

例子

Go 语言中的同步

Do not communicate by sharing memory; instead, share memory by communicating. ——Effective Go

共享内存 = 万恶之源

  • 信号量/条件变量:实现了同步,但没有实现 “通信”
    • 数据传递完全靠手工 (没上锁就错了)

但 UNIX 时代就有一个实现并行的机制了

  • cat *.txt | wc -l
    • 管道是一个天然的生产者/消费者!
    • 为什么不用 “管道” 实现协程/线程间的同步 + 通信呢?
      • Channels in Go